From bc0d82617f51e9f2cc007ba5799b8932c991c001 Mon Sep 17 00:00:00 2001 From: XDEV Renovate Bot Date: Fri, 19 Jun 2026 05:54:58 +0000 Subject: [PATCH 001/141] Update actions/checkout action to v7 --- .github/workflows/broken-links.yml | 2 +- .github/workflows/report-gha-workflow-security-problems.yml | 2 +- .github/workflows/sync-labels.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/broken-links.yml b/.github/workflows/broken-links.yml index ce623e98..a6c3ed5c 100644 --- a/.github/workflows/broken-links.yml +++ b/.github/workflows/broken-links.yml @@ -12,7 +12,7 @@ jobs: link-checker: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - run: mv .github/.lycheeignore .lycheeignore diff --git a/.github/workflows/report-gha-workflow-security-problems.yml b/.github/workflows/report-gha-workflow-security-problems.yml index b17aa539..78470285 100644 --- a/.github/workflows/report-gha-workflow-security-problems.yml +++ b/.github/workflows/report-gha-workflow-security-problems.yml @@ -17,7 +17,7 @@ jobs: # Only run this in our repos (Prevent notification spam by forks) if: ${{ github.repository_owner == 'xdev-software' }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Check id: check diff --git a/.github/workflows/sync-labels.yml b/.github/workflows/sync-labels.yml index c43365ef..00e40162 100644 --- a/.github/workflows/sync-labels.yml +++ b/.github/workflows/sync-labels.yml @@ -15,7 +15,7 @@ jobs: labels: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: sparse-checkout: .github/labels.yml From 3734ffa515c12f5262c94d4f8cd6fd163978a19a Mon Sep 17 00:00:00 2001 From: XDEV Renovate Bot Date: Sat, 27 Jun 2026 05:29:44 +0000 Subject: [PATCH 002/141] Update dependency ch.qos.logback:logback-classic to v1.5.37 --- base-demo/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base-demo/pom.xml b/base-demo/pom.xml index d10c5824..bea6ca8a 100644 --- a/base-demo/pom.xml +++ b/base-demo/pom.xml @@ -45,7 +45,7 @@ ch.qos.logback logback-classic - 1.5.36 + 1.5.37 test From b2045ad13e6142f3643e5a11e9385f0081e6ae85 Mon Sep 17 00:00:00 2001 From: XDEV Renovate Bot Date: Sat, 27 Jun 2026 05:29:52 +0000 Subject: [PATCH 003/141] Update dependency org.sonatype.central:central-publishing-maven-plugin to v0.11.0 --- db-jdbc-spring-orm-eclipselink/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db-jdbc-spring-orm-eclipselink/pom.xml b/db-jdbc-spring-orm-eclipselink/pom.xml index 3508dd9e..73350db1 100644 --- a/db-jdbc-spring-orm-eclipselink/pom.xml +++ b/db-jdbc-spring-orm-eclipselink/pom.xml @@ -226,7 +226,7 @@ org.sonatype.central central-publishing-maven-plugin - 0.10.0 + 0.11.0 true sonatype-central-portal From 04c6e78a45c68127518dc31ddbe486931511aae5 Mon Sep 17 00:00:00 2001 From: XDEV Renovate Bot Date: Sun, 28 Jun 2026 05:42:31 +0000 Subject: [PATCH 004/141] Update org.hibernate.orm to v7.4.3.Final --- db-jdbc-spring-orm-hibernate/pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/db-jdbc-spring-orm-hibernate/pom.xml b/db-jdbc-spring-orm-hibernate/pom.xml index 5d61a77c..35bf0a4d 100644 --- a/db-jdbc-spring-orm-hibernate/pom.xml +++ b/db-jdbc-spring-orm-hibernate/pom.xml @@ -59,18 +59,18 @@ org.hibernate.orm hibernate-core - 7.4.2.Final + 7.4.3.Final org.hibernate.orm hibernate-scan-jandex - 7.4.2.Final + 7.4.3.Final org.hibernate.orm hibernate-hikaricp - 7.4.2.Final + 7.4.3.Final From bbc7ec47a5287a96fb3fc544dbaf879c3e35751f Mon Sep 17 00:00:00 2001 From: XDEV Renovate Bot Date: Mon, 29 Jun 2026 05:52:24 +0000 Subject: [PATCH 005/141] Update dependency com.puppycrawl.tools:checkstyle to v13.7.0 --- pom.xml | 2 +- template-placeholder/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index bb0d7cd9..193ce997 100644 --- a/pom.xml +++ b/pom.xml @@ -45,7 +45,7 @@ com.puppycrawl.tools checkstyle - 13.6.0 + 13.7.0 diff --git a/template-placeholder/pom.xml b/template-placeholder/pom.xml index 4b2b7364..c45d74f2 100644 --- a/template-placeholder/pom.xml +++ b/template-placeholder/pom.xml @@ -215,7 +215,7 @@ com.puppycrawl.tools checkstyle - 13.6.0 + 13.7.0 From 8de5826a064bc8bcdbc4db9453addbe2acca1ed2 Mon Sep 17 00:00:00 2001 From: XDEV Renovate Bot Date: Mon, 29 Jun 2026 05:52:45 +0000 Subject: [PATCH 006/141] Update org.junit to v6.1.1 --- advanced-demo/integration-tests/pom.xml | 2 +- advanced-demo/pom.xml | 2 +- base-demo/pom.xml | 2 +- base/pom.xml | 4 ++-- db-jdbc/pom.xml | 2 +- junit-jupiter-api-support/pom.xml | 2 +- spring-dao-support/pom.xml | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/advanced-demo/integration-tests/pom.xml b/advanced-demo/integration-tests/pom.xml index 900e70f7..c0b3c112 100644 --- a/advanced-demo/integration-tests/pom.xml +++ b/advanced-demo/integration-tests/pom.xml @@ -109,7 +109,7 @@ org.junit junit-bom - 6.1.0 + 6.1.1 pom import diff --git a/advanced-demo/pom.xml b/advanced-demo/pom.xml index 4425da10..2f58a665 100644 --- a/advanced-demo/pom.xml +++ b/advanced-demo/pom.xml @@ -70,7 +70,7 @@ org.junit junit-bom - 6.1.0 + 6.1.1 pom import diff --git a/base-demo/pom.xml b/base-demo/pom.xml index d10c5824..8da70954 100644 --- a/base-demo/pom.xml +++ b/base-demo/pom.xml @@ -38,7 +38,7 @@ org.junit.jupiter junit-jupiter - 6.1.0 + 6.1.1 test diff --git a/base/pom.xml b/base/pom.xml index 853c63e2..0b2ae0cb 100644 --- a/base/pom.xml +++ b/base/pom.xml @@ -69,14 +69,14 @@ org.junit.platform junit-platform-launcher compile - 6.1.0 + 6.1.1 org.junit.jupiter junit-jupiter - 6.1.0 + 6.1.1 test diff --git a/db-jdbc/pom.xml b/db-jdbc/pom.xml index 34598840..c82d5923 100644 --- a/db-jdbc/pom.xml +++ b/db-jdbc/pom.xml @@ -72,7 +72,7 @@ org.junit.jupiter junit-jupiter - 6.1.0 + 6.1.1 test diff --git a/junit-jupiter-api-support/pom.xml b/junit-jupiter-api-support/pom.xml index 1d5a9864..6654bd9d 100644 --- a/junit-jupiter-api-support/pom.xml +++ b/junit-jupiter-api-support/pom.xml @@ -53,7 +53,7 @@ org.junit.jupiter junit-jupiter-api - 6.1.0 + 6.1.1 compile diff --git a/spring-dao-support/pom.xml b/spring-dao-support/pom.xml index a5c9d8dc..c2034bf2 100644 --- a/spring-dao-support/pom.xml +++ b/spring-dao-support/pom.xml @@ -78,7 +78,7 @@ org.junit.jupiter junit-jupiter - 6.1.0 + 6.1.1 test From c60dd67e62cc23feb374571043d4c9b9d8e02485 Mon Sep 17 00:00:00 2001 From: XDEV Renovate Bot Date: Mon, 29 Jun 2026 05:52:49 +0000 Subject: [PATCH 007/141] Update dependency com.puppycrawl.tools:checkstyle to v13.7.0 --- advanced-demo/pom.xml | 2 +- base/pom.xml | 2 +- db-jdbc-spring-orm-eclipselink/pom.xml | 2 +- db-jdbc-spring-orm-hibernate/pom.xml | 2 +- db-jdbc-spring-orm/pom.xml | 2 +- db-jdbc/pom.xml | 2 +- jacoco/pom.xml | 2 +- jul-to-slf4j/pom.xml | 2 +- junit-jupiter-api-support/pom.xml | 2 +- mockserver/pom.xml | 2 +- oidc-server-mock/pom.xml | 2 +- pom.xml | 2 +- selenium/pom.xml | 2 +- spring-dao-support/pom.xml | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/advanced-demo/pom.xml b/advanced-demo/pom.xml index 4425da10..1563e37b 100644 --- a/advanced-demo/pom.xml +++ b/advanced-demo/pom.xml @@ -195,7 +195,7 @@ com.puppycrawl.tools checkstyle - 13.6.0 + 13.7.0 diff --git a/base/pom.xml b/base/pom.xml index 853c63e2..efc5d64b 100644 --- a/base/pom.xml +++ b/base/pom.xml @@ -284,7 +284,7 @@ com.puppycrawl.tools checkstyle - 13.6.0 + 13.7.0 diff --git a/db-jdbc-spring-orm-eclipselink/pom.xml b/db-jdbc-spring-orm-eclipselink/pom.xml index 3508dd9e..b3ba191e 100644 --- a/db-jdbc-spring-orm-eclipselink/pom.xml +++ b/db-jdbc-spring-orm-eclipselink/pom.xml @@ -248,7 +248,7 @@ com.puppycrawl.tools checkstyle - 13.5.0 + 13.7.0 diff --git a/db-jdbc-spring-orm-hibernate/pom.xml b/db-jdbc-spring-orm-hibernate/pom.xml index 5d61a77c..46bdf5d8 100644 --- a/db-jdbc-spring-orm-hibernate/pom.xml +++ b/db-jdbc-spring-orm-hibernate/pom.xml @@ -259,7 +259,7 @@ com.puppycrawl.tools checkstyle - 13.6.0 + 13.7.0 diff --git a/db-jdbc-spring-orm/pom.xml b/db-jdbc-spring-orm/pom.xml index 30410169..d4ae469d 100644 --- a/db-jdbc-spring-orm/pom.xml +++ b/db-jdbc-spring-orm/pom.xml @@ -258,7 +258,7 @@ com.puppycrawl.tools checkstyle - 13.6.0 + 13.7.0 diff --git a/db-jdbc/pom.xml b/db-jdbc/pom.xml index 34598840..e0e9e1f1 100644 --- a/db-jdbc/pom.xml +++ b/db-jdbc/pom.xml @@ -268,7 +268,7 @@ com.puppycrawl.tools checkstyle - 13.6.0 + 13.7.0 diff --git a/jacoco/pom.xml b/jacoco/pom.xml index 250c1adf..d59499dc 100644 --- a/jacoco/pom.xml +++ b/jacoco/pom.xml @@ -242,7 +242,7 @@ com.puppycrawl.tools checkstyle - 13.6.0 + 13.7.0 diff --git a/jul-to-slf4j/pom.xml b/jul-to-slf4j/pom.xml index 975557c2..2015b543 100644 --- a/jul-to-slf4j/pom.xml +++ b/jul-to-slf4j/pom.xml @@ -242,7 +242,7 @@ com.puppycrawl.tools checkstyle - 13.6.0 + 13.7.0 diff --git a/junit-jupiter-api-support/pom.xml b/junit-jupiter-api-support/pom.xml index 1d5a9864..9aea46d1 100644 --- a/junit-jupiter-api-support/pom.xml +++ b/junit-jupiter-api-support/pom.xml @@ -243,7 +243,7 @@ com.puppycrawl.tools checkstyle - 13.6.0 + 13.7.0 diff --git a/mockserver/pom.xml b/mockserver/pom.xml index 7a908c8f..a1483c80 100644 --- a/mockserver/pom.xml +++ b/mockserver/pom.xml @@ -264,7 +264,7 @@ com.puppycrawl.tools checkstyle - 13.6.0 + 13.7.0 diff --git a/oidc-server-mock/pom.xml b/oidc-server-mock/pom.xml index 25ba063d..b62d973d 100644 --- a/oidc-server-mock/pom.xml +++ b/oidc-server-mock/pom.xml @@ -248,7 +248,7 @@ com.puppycrawl.tools checkstyle - 13.6.0 + 13.7.0 diff --git a/pom.xml b/pom.xml index 16a6ad85..9fd2df1f 100644 --- a/pom.xml +++ b/pom.xml @@ -77,7 +77,7 @@ com.puppycrawl.tools checkstyle - 13.6.0 + 13.7.0 diff --git a/selenium/pom.xml b/selenium/pom.xml index 9ae5d3ca..2a50cd7b 100644 --- a/selenium/pom.xml +++ b/selenium/pom.xml @@ -296,7 +296,7 @@ com.puppycrawl.tools checkstyle - 13.6.0 + 13.7.0 diff --git a/spring-dao-support/pom.xml b/spring-dao-support/pom.xml index a5c9d8dc..36933141 100644 --- a/spring-dao-support/pom.xml +++ b/spring-dao-support/pom.xml @@ -274,7 +274,7 @@ com.puppycrawl.tools checkstyle - 13.6.0 + 13.7.0 From bf72b0de3e9b032dbbe6375813b23e585f01ebfe Mon Sep 17 00:00:00 2001 From: XDEV Renovate Bot Date: Tue, 30 Jun 2026 05:37:37 +0000 Subject: [PATCH 008/141] Update net.sourceforge.pmd to v7.26.0 --- pom.xml | 4 ++-- template-placeholder/pom.xml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index bb0d7cd9..8a183196 100644 --- a/pom.xml +++ b/pom.xml @@ -83,12 +83,12 @@ net.sourceforge.pmd pmd-core - 7.25.0 + 7.26.0 net.sourceforge.pmd pmd-java - 7.25.0 + 7.26.0 diff --git a/template-placeholder/pom.xml b/template-placeholder/pom.xml index 4b2b7364..b083f7df 100644 --- a/template-placeholder/pom.xml +++ b/template-placeholder/pom.xml @@ -253,12 +253,12 @@ net.sourceforge.pmd pmd-core - 7.25.0 + 7.26.0 net.sourceforge.pmd pmd-java - 7.25.0 + 7.26.0 From c09cbbb07c2d65f6d9f72d76c85ca25bf5a22f86 Mon Sep 17 00:00:00 2001 From: XDEV Renovate Bot Date: Tue, 30 Jun 2026 05:37:49 +0000 Subject: [PATCH 009/141] Update dependency org.apache.httpcomponents.client5:httpclient5 to v5.6.2 --- oidc-server-mock/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oidc-server-mock/pom.xml b/oidc-server-mock/pom.xml index 25ba063d..56caf0e1 100644 --- a/oidc-server-mock/pom.xml +++ b/oidc-server-mock/pom.xml @@ -59,7 +59,7 @@ org.apache.httpcomponents.client5 httpclient5 - 5.6.1 + 5.6.2 From 5343c2ad228a1ab34ac31f1c1eeb64663ff0d6bb Mon Sep 17 00:00:00 2001 From: XDEV Renovate Bot Date: Tue, 30 Jun 2026 05:37:52 +0000 Subject: [PATCH 010/141] Update dependency org.eclipse.persistence:eclipselink to v5.0.1 --- db-jdbc-spring-orm-eclipselink/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db-jdbc-spring-orm-eclipselink/pom.xml b/db-jdbc-spring-orm-eclipselink/pom.xml index 3508dd9e..284cf60a 100644 --- a/db-jdbc-spring-orm-eclipselink/pom.xml +++ b/db-jdbc-spring-orm-eclipselink/pom.xml @@ -59,7 +59,7 @@ org.eclipse.persistence eclipselink - 5.0.0 + 5.0.1 From 8d68bc3c54a0cbba5b294042fe4fc2aa3fde5954 Mon Sep 17 00:00:00 2001 From: XDEV Renovate Bot Date: Tue, 30 Jun 2026 05:37:56 +0000 Subject: [PATCH 011/141] Update net.sourceforge.pmd to v7.26.0 --- advanced-demo/pom.xml | 4 ++-- base/pom.xml | 4 ++-- db-jdbc-spring-orm-eclipselink/pom.xml | 4 ++-- db-jdbc-spring-orm-hibernate/pom.xml | 4 ++-- db-jdbc-spring-orm/pom.xml | 4 ++-- db-jdbc/pom.xml | 4 ++-- jacoco/pom.xml | 4 ++-- jul-to-slf4j/pom.xml | 4 ++-- junit-jupiter-api-support/pom.xml | 4 ++-- mockserver/pom.xml | 4 ++-- oidc-server-mock/pom.xml | 4 ++-- pom.xml | 4 ++-- selenium/pom.xml | 4 ++-- spring-dao-support/pom.xml | 4 ++-- 14 files changed, 28 insertions(+), 28 deletions(-) diff --git a/advanced-demo/pom.xml b/advanced-demo/pom.xml index 4425da10..746e99e5 100644 --- a/advanced-demo/pom.xml +++ b/advanced-demo/pom.xml @@ -236,12 +236,12 @@ net.sourceforge.pmd pmd-core - 7.25.0 + 7.26.0 net.sourceforge.pmd pmd-java - 7.25.0 + 7.26.0 diff --git a/base/pom.xml b/base/pom.xml index 853c63e2..e29748c2 100644 --- a/base/pom.xml +++ b/base/pom.xml @@ -322,12 +322,12 @@ net.sourceforge.pmd pmd-core - 7.25.0 + 7.26.0 net.sourceforge.pmd pmd-java - 7.25.0 + 7.26.0 diff --git a/db-jdbc-spring-orm-eclipselink/pom.xml b/db-jdbc-spring-orm-eclipselink/pom.xml index 3508dd9e..4efbfed9 100644 --- a/db-jdbc-spring-orm-eclipselink/pom.xml +++ b/db-jdbc-spring-orm-eclipselink/pom.xml @@ -286,12 +286,12 @@ net.sourceforge.pmd pmd-core - 7.25.0 + 7.26.0 net.sourceforge.pmd pmd-java - 7.25.0 + 7.26.0 diff --git a/db-jdbc-spring-orm-hibernate/pom.xml b/db-jdbc-spring-orm-hibernate/pom.xml index 5d61a77c..b0e1171e 100644 --- a/db-jdbc-spring-orm-hibernate/pom.xml +++ b/db-jdbc-spring-orm-hibernate/pom.xml @@ -297,12 +297,12 @@ net.sourceforge.pmd pmd-core - 7.25.0 + 7.26.0 net.sourceforge.pmd pmd-java - 7.25.0 + 7.26.0 diff --git a/db-jdbc-spring-orm/pom.xml b/db-jdbc-spring-orm/pom.xml index 30410169..73f02ae6 100644 --- a/db-jdbc-spring-orm/pom.xml +++ b/db-jdbc-spring-orm/pom.xml @@ -295,12 +295,12 @@ net.sourceforge.pmd pmd-core - 7.25.0 + 7.26.0 net.sourceforge.pmd pmd-java - 7.25.0 + 7.26.0 diff --git a/db-jdbc/pom.xml b/db-jdbc/pom.xml index 34598840..d019aea2 100644 --- a/db-jdbc/pom.xml +++ b/db-jdbc/pom.xml @@ -306,12 +306,12 @@ net.sourceforge.pmd pmd-core - 7.25.0 + 7.26.0 net.sourceforge.pmd pmd-java - 7.25.0 + 7.26.0 diff --git a/jacoco/pom.xml b/jacoco/pom.xml index 250c1adf..cc1b07ac 100644 --- a/jacoco/pom.xml +++ b/jacoco/pom.xml @@ -280,12 +280,12 @@ net.sourceforge.pmd pmd-core - 7.25.0 + 7.26.0 net.sourceforge.pmd pmd-java - 7.25.0 + 7.26.0 diff --git a/jul-to-slf4j/pom.xml b/jul-to-slf4j/pom.xml index 975557c2..70f6b157 100644 --- a/jul-to-slf4j/pom.xml +++ b/jul-to-slf4j/pom.xml @@ -280,12 +280,12 @@ net.sourceforge.pmd pmd-core - 7.25.0 + 7.26.0 net.sourceforge.pmd pmd-java - 7.25.0 + 7.26.0 diff --git a/junit-jupiter-api-support/pom.xml b/junit-jupiter-api-support/pom.xml index 1d5a9864..29ce4039 100644 --- a/junit-jupiter-api-support/pom.xml +++ b/junit-jupiter-api-support/pom.xml @@ -281,12 +281,12 @@ net.sourceforge.pmd pmd-core - 7.25.0 + 7.26.0 net.sourceforge.pmd pmd-java - 7.25.0 + 7.26.0 diff --git a/mockserver/pom.xml b/mockserver/pom.xml index 7a908c8f..fbe8b100 100644 --- a/mockserver/pom.xml +++ b/mockserver/pom.xml @@ -302,12 +302,12 @@ net.sourceforge.pmd pmd-core - 7.25.0 + 7.26.0 net.sourceforge.pmd pmd-java - 7.25.0 + 7.26.0 diff --git a/oidc-server-mock/pom.xml b/oidc-server-mock/pom.xml index 25ba063d..89d28900 100644 --- a/oidc-server-mock/pom.xml +++ b/oidc-server-mock/pom.xml @@ -286,12 +286,12 @@ net.sourceforge.pmd pmd-core - 7.25.0 + 7.26.0 net.sourceforge.pmd pmd-java - 7.25.0 + 7.26.0 diff --git a/pom.xml b/pom.xml index 16a6ad85..7a4570aa 100644 --- a/pom.xml +++ b/pom.xml @@ -115,12 +115,12 @@ net.sourceforge.pmd pmd-core - 7.25.0 + 7.26.0 net.sourceforge.pmd pmd-java - 7.25.0 + 7.26.0 diff --git a/selenium/pom.xml b/selenium/pom.xml index 9ae5d3ca..55b68bcc 100644 --- a/selenium/pom.xml +++ b/selenium/pom.xml @@ -334,12 +334,12 @@ net.sourceforge.pmd pmd-core - 7.25.0 + 7.26.0 net.sourceforge.pmd pmd-java - 7.25.0 + 7.26.0 diff --git a/spring-dao-support/pom.xml b/spring-dao-support/pom.xml index a5c9d8dc..a7d0d6fa 100644 --- a/spring-dao-support/pom.xml +++ b/spring-dao-support/pom.xml @@ -312,12 +312,12 @@ net.sourceforge.pmd pmd-core - 7.25.0 + 7.26.0 net.sourceforge.pmd pmd-java - 7.25.0 + 7.26.0 From 793d3112bd42c9d5a5857fe45d06c9ecc0c7f3bc Mon Sep 17 00:00:00 2001 From: AB Date: Wed, 1 Jul 2026 07:50:24 +0200 Subject: [PATCH 012/141] Enable new PMD 7.26 rule --- .config/pmd/java/ruleset.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/.config/pmd/java/ruleset.xml b/.config/pmd/java/ruleset.xml index b6d43e51..8dde42bc 100644 --- a/.config/pmd/java/ruleset.xml +++ b/.config/pmd/java/ruleset.xml @@ -141,6 +141,7 @@ + From 0fb2aeb6cd624b28815850fa5961d05defefc747 Mon Sep 17 00:00:00 2001 From: AB Date: Wed, 1 Jul 2026 08:02:30 +0200 Subject: [PATCH 013/141] CheckStyle-IDEA: Always use lastest (bundled) version This prevents annoying popups whenever the plugin updates --- .idea/checkstyle-idea.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.idea/checkstyle-idea.xml b/.idea/checkstyle-idea.xml index 3dbbcc7a..b8b753e6 100644 --- a/.idea/checkstyle-idea.xml +++ b/.idea/checkstyle-idea.xml @@ -1,7 +1,7 @@ - 13.5.0 + latest JavaOnlyWithTests true true From 35b99ab618ad1bd0aa03bf36b9881ef96de07340 Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 2 Jul 2026 14:41:52 +0200 Subject: [PATCH 014/141] Improve db-jdbc-spring-orm --- CHANGELOG.md | 5 +++ ...pseLinkEntityManagerControllerFactory.java | 12 ++---- ...bernateEntityManagerControllerFactory.java | 20 ++------- .../SpringEntityManagerControllerFactory.java | 43 ++++++++++++++++--- 4 files changed, 47 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b428b456..79ae1a63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +# 3.5.0 +* `db-jdbc-spring-orm` & implementations + * No longer use `setPersistenceProviderClassName` because it has no effect + * By default create `PersistenceProvider` only once to make it possible to cache redundant calls + # 3.4.1 * Remove dedicated code for Java 17 (no longer needed because Java 21 is the minimum) diff --git a/db-jdbc-spring-orm-eclipselink/src/main/java/software/xdev/tci/db/persistence/eclipselink/EclipseLinkEntityManagerControllerFactory.java b/db-jdbc-spring-orm-eclipselink/src/main/java/software/xdev/tci/db/persistence/eclipselink/EclipseLinkEntityManagerControllerFactory.java index 7308aab3..a344b7da 100644 --- a/db-jdbc-spring-orm-eclipselink/src/main/java/software/xdev/tci/db/persistence/eclipselink/EclipseLinkEntityManagerControllerFactory.java +++ b/db-jdbc-spring-orm-eclipselink/src/main/java/software/xdev/tci/db/persistence/eclipselink/EclipseLinkEntityManagerControllerFactory.java @@ -19,9 +19,6 @@ import java.util.Set; import java.util.function.Supplier; -import jakarta.persistence.EntityManagerFactory; -import jakarta.persistence.spi.PersistenceUnitInfo; - import org.eclipse.persistence.config.PersistenceUnitProperties; import org.eclipse.persistence.jpa.PersistenceProvider; import org.springframework.orm.jpa.persistenceunit.SpringPersistenceUnitInfo; @@ -30,7 +27,7 @@ public class EclipseLinkEntityManagerControllerFactory - extends SpringEntityManagerControllerFactory + extends SpringEntityManagerControllerFactory { // Disabled (false) by default as it's not used by the default created SpringPersistenceUnitInfo anyway protected boolean weavingEnabled; @@ -69,7 +66,6 @@ protected SpringPersistenceUnitInfo createSpringPersistenceUnitInfo() final SpringPersistenceUnitInfo spui = super.createSpringPersistenceUnitInfo(); // Required otherwise createContainerEntityManagerFactoryImpl crashes spui.setPersistenceUnitRootUrl(this.getClass().getResource("")); - spui.setPersistenceProviderClassName(PersistenceProvider.class.getName()); return spui; } @@ -85,10 +81,8 @@ protected Map buildProperties() } @Override - protected EntityManagerFactory createEntityManagerFactory( - final PersistenceUnitInfo pui, - final Map properties) + protected PersistenceProvider createDefaultPersistenceProvider() { - return new PersistenceProvider().createContainerEntityManagerFactory(pui, properties); + return new PersistenceProvider(); } } diff --git a/db-jdbc-spring-orm-hibernate/src/main/java/software/xdev/tci/db/persistence/hibernate/HibernateEntityManagerControllerFactory.java b/db-jdbc-spring-orm-hibernate/src/main/java/software/xdev/tci/db/persistence/hibernate/HibernateEntityManagerControllerFactory.java index 49fc9332..b2161daf 100644 --- a/db-jdbc-spring-orm-hibernate/src/main/java/software/xdev/tci/db/persistence/hibernate/HibernateEntityManagerControllerFactory.java +++ b/db-jdbc-spring-orm-hibernate/src/main/java/software/xdev/tci/db/persistence/hibernate/HibernateEntityManagerControllerFactory.java @@ -20,20 +20,16 @@ import java.util.Set; import java.util.function.Supplier; -import jakarta.persistence.EntityManagerFactory; -import jakarta.persistence.spi.PersistenceUnitInfo; - import org.hibernate.cfg.JdbcSettings; import org.hibernate.cfg.PersistenceSettings; import org.hibernate.hikaricp.internal.HikariCPConnectionProvider; import org.hibernate.jpa.HibernatePersistenceProvider; -import org.springframework.orm.jpa.persistenceunit.SpringPersistenceUnitInfo; import software.xdev.tci.db.persistence.SpringEntityManagerControllerFactory; public class HibernateEntityManagerControllerFactory - extends SpringEntityManagerControllerFactory + extends SpringEntityManagerControllerFactory { protected String connectionProviderClassName = HikariCPConnectionProvider.class.getName(); protected boolean disableHibernateFormatter = true; @@ -69,14 +65,6 @@ public HibernateEntityManagerControllerFactory withUseCachingStandardScanner( return this; } - @Override - protected SpringPersistenceUnitInfo createSpringPersistenceUnitInfo() - { - final SpringPersistenceUnitInfo spui = super.createSpringPersistenceUnitInfo(); - spui.setPersistenceProviderClassName(HibernatePersistenceProvider.class.getName()); - return spui; - } - @Override protected Map buildProperties() { @@ -95,10 +83,8 @@ protected Map buildProperties() } @Override - protected EntityManagerFactory createEntityManagerFactory( - final PersistenceUnitInfo pui, - final Map properties) + protected HibernatePersistenceProvider createDefaultPersistenceProvider() { - return new HibernatePersistenceProvider().createContainerEntityManagerFactory(pui, properties); + return new HibernatePersistenceProvider(); } } diff --git a/db-jdbc-spring-orm/src/main/java/software/xdev/tci/db/persistence/SpringEntityManagerControllerFactory.java b/db-jdbc-spring-orm/src/main/java/software/xdev/tci/db/persistence/SpringEntityManagerControllerFactory.java index 8231b06d..58d49639 100644 --- a/db-jdbc-spring-orm/src/main/java/software/xdev/tci/db/persistence/SpringEntityManagerControllerFactory.java +++ b/db-jdbc-spring-orm/src/main/java/software/xdev/tci/db/persistence/SpringEntityManagerControllerFactory.java @@ -20,21 +20,27 @@ import java.net.URL; import java.util.Collection; import java.util.Collections; +import java.util.Map; import java.util.Set; import java.util.function.Supplier; +import jakarta.persistence.EntityManagerFactory; import jakarta.persistence.PersistenceUnitTransactionType; import jakarta.persistence.spi.ClassTransformer; +import jakarta.persistence.spi.PersistenceProvider; import jakarta.persistence.spi.PersistenceUnitInfo; import org.springframework.orm.jpa.persistenceunit.SpringPersistenceUnitInfo; @SuppressWarnings("java:S119") -public abstract class SpringEntityManagerControllerFactory> +public abstract class SpringEntityManagerControllerFactory< + P extends PersistenceProvider, + SELF extends SpringEntityManagerControllerFactory> extends EntityManagerControllerFactory { protected boolean addJarFileUrls; + protected P persistenceProvider; protected SpringEntityManagerControllerFactory() { @@ -61,6 +67,15 @@ public SELF withAddJarFileUrls(final boolean addJarFileUrls) return this.self(); } + /** + * Allows to manually configure a (default) persistence provider + */ + public SELF withPersistenceProvider(final P persistenceProvider) + { + this.persistenceProvider = persistenceProvider; + return this.self(); + } + @SuppressWarnings("java:S4449") // can't add annotations to source code that we do not control protected SpringPersistenceUnitInfo instantiateSpringPersistenceUnitInfo() { @@ -100,12 +115,6 @@ protected SpringPersistenceUnitInfo createSpringPersistenceUnitInfo() return pui; } - @Override - protected PersistenceUnitInfo createPersistenceUnitInfo() - { - return this.createSpringPersistenceUnitInfo().asStandardPersistenceUnitInfo(); - } - protected Collection jarFileUrlsToAdd() { try @@ -119,4 +128,24 @@ protected Collection jarFileUrlsToAdd() throw new UncheckedIOException(ioe); } } + + @Override + protected PersistenceUnitInfo createPersistenceUnitInfo() + { + return this.createSpringPersistenceUnitInfo().asStandardPersistenceUnitInfo(); + } + + protected abstract P createDefaultPersistenceProvider(); + + @Override + protected EntityManagerFactory createEntityManagerFactory( + final PersistenceUnitInfo pui, + final Map properties) + { + if(this.persistenceProvider == null) + { + this.persistenceProvider = this.createDefaultPersistenceProvider(); + } + return this.persistenceProvider.createContainerEntityManagerFactory(pui, properties); + } } From 6118a35d6d50a422b2d06ad0d255d3a3f0d74be2 Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 2 Jul 2026 14:42:01 +0200 Subject: [PATCH 015/141] Bump version --- advanced-demo/entities-metamodel/pom.xml | 2 +- advanced-demo/entities/pom.xml | 2 +- .../persistence-it-eclipselink/pom.xml | 2 +- .../persistence-it-hibernate/pom.xml | 2 +- .../integration-tests/persistence-it/pom.xml | 2 +- advanced-demo/integration-tests/pom.xml | 10 +++---- .../integration-tests/tci-db/pom.xml | 2 +- .../integration-tests/tci-webapp/pom.xml | 2 +- .../integration-tests/webapp-it/pom.xml | 2 +- advanced-demo/persistence/pom.xml | 2 +- advanced-demo/pom.xml | 8 +++--- advanced-demo/webapp/pom.xml | 2 +- base-demo/pom.xml | 4 +-- base/pom.xml | 2 +- bom/pom.xml | 26 +++++++++---------- db-jdbc-spring-orm-eclipselink/pom.xml | 4 +-- db-jdbc-spring-orm-hibernate/pom.xml | 4 +-- db-jdbc-spring-orm/pom.xml | 4 +-- db-jdbc/pom.xml | 4 +-- jacoco/pom.xml | 4 +-- jul-to-slf4j/pom.xml | 2 +- junit-jupiter-api-support/pom.xml | 2 +- mockserver/pom.xml | 4 +-- oidc-server-mock/pom.xml | 4 +-- pom.xml | 2 +- selenium/pom.xml | 8 +++--- spring-dao-support/pom.xml | 4 +-- 27 files changed, 58 insertions(+), 58 deletions(-) diff --git a/advanced-demo/entities-metamodel/pom.xml b/advanced-demo/entities-metamodel/pom.xml index 034a922d..73d24334 100644 --- a/advanced-demo/entities-metamodel/pom.xml +++ b/advanced-demo/entities-metamodel/pom.xml @@ -7,7 +7,7 @@ software.xdev.tci.demo advanced-demo - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT entities-metamodel diff --git a/advanced-demo/entities/pom.xml b/advanced-demo/entities/pom.xml index 13668e87..c5975091 100644 --- a/advanced-demo/entities/pom.xml +++ b/advanced-demo/entities/pom.xml @@ -7,7 +7,7 @@ software.xdev.tci.demo advanced-demo - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT entities diff --git a/advanced-demo/integration-tests/persistence-it-eclipselink/pom.xml b/advanced-demo/integration-tests/persistence-it-eclipselink/pom.xml index 3e4cbacb..0062e2db 100644 --- a/advanced-demo/integration-tests/persistence-it-eclipselink/pom.xml +++ b/advanced-demo/integration-tests/persistence-it-eclipselink/pom.xml @@ -7,7 +7,7 @@ software.xdev.tci.demo.it integration-tests - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT persistence-it-eclipselink diff --git a/advanced-demo/integration-tests/persistence-it-hibernate/pom.xml b/advanced-demo/integration-tests/persistence-it-hibernate/pom.xml index e4c6b8d9..8728d38d 100644 --- a/advanced-demo/integration-tests/persistence-it-hibernate/pom.xml +++ b/advanced-demo/integration-tests/persistence-it-hibernate/pom.xml @@ -7,7 +7,7 @@ software.xdev.tci.demo.it integration-tests - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT persistence-it-hibernate diff --git a/advanced-demo/integration-tests/persistence-it/pom.xml b/advanced-demo/integration-tests/persistence-it/pom.xml index 5e44cb8e..b3b2f7d8 100644 --- a/advanced-demo/integration-tests/persistence-it/pom.xml +++ b/advanced-demo/integration-tests/persistence-it/pom.xml @@ -7,7 +7,7 @@ software.xdev.tci.demo.it integration-tests - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT persistence-it diff --git a/advanced-demo/integration-tests/pom.xml b/advanced-demo/integration-tests/pom.xml index 900e70f7..de891695 100644 --- a/advanced-demo/integration-tests/pom.xml +++ b/advanced-demo/integration-tests/pom.xml @@ -7,12 +7,12 @@ software.xdev.tci.demo advanced-demo - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT software.xdev.tci.demo.it integration-tests - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT pom @@ -30,7 +30,7 @@ software.xdev.tci.demo.it persistence-it - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT test-jar tests @@ -38,13 +38,13 @@ software.xdev.tci.demo.it tci-db - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT software.xdev.tci.demo.it tci-webapp - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT diff --git a/advanced-demo/integration-tests/tci-db/pom.xml b/advanced-demo/integration-tests/tci-db/pom.xml index 5cd6248c..8de104dc 100644 --- a/advanced-demo/integration-tests/tci-db/pom.xml +++ b/advanced-demo/integration-tests/tci-db/pom.xml @@ -7,7 +7,7 @@ software.xdev.tci.demo.it integration-tests - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT tci-db diff --git a/advanced-demo/integration-tests/tci-webapp/pom.xml b/advanced-demo/integration-tests/tci-webapp/pom.xml index affcb9cd..39fa8d66 100644 --- a/advanced-demo/integration-tests/tci-webapp/pom.xml +++ b/advanced-demo/integration-tests/tci-webapp/pom.xml @@ -7,7 +7,7 @@ software.xdev.tci.demo.it integration-tests - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT tci-webapp diff --git a/advanced-demo/integration-tests/webapp-it/pom.xml b/advanced-demo/integration-tests/webapp-it/pom.xml index e2c0a2a1..1cfe501e 100644 --- a/advanced-demo/integration-tests/webapp-it/pom.xml +++ b/advanced-demo/integration-tests/webapp-it/pom.xml @@ -7,7 +7,7 @@ software.xdev.tci.demo.it integration-tests - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT webapp-it diff --git a/advanced-demo/persistence/pom.xml b/advanced-demo/persistence/pom.xml index 27577c3b..e9fb9f28 100644 --- a/advanced-demo/persistence/pom.xml +++ b/advanced-demo/persistence/pom.xml @@ -7,7 +7,7 @@ software.xdev.tci.demo advanced-demo - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT persistence diff --git a/advanced-demo/pom.xml b/advanced-demo/pom.xml index 4425da10..18b2779e 100644 --- a/advanced-demo/pom.xml +++ b/advanced-demo/pom.xml @@ -6,7 +6,7 @@ software.xdev.tci.demo advanced-demo - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT pom @@ -38,19 +38,19 @@ software.xdev.tci.demo entities - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT software.xdev.tci.demo entities-metamodel - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT software.xdev.tci.demo persistence - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT diff --git a/advanced-demo/webapp/pom.xml b/advanced-demo/webapp/pom.xml index 0a831534..4a0f2d57 100644 --- a/advanced-demo/webapp/pom.xml +++ b/advanced-demo/webapp/pom.xml @@ -7,7 +7,7 @@ software.xdev.tci.demo advanced-demo - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT webapp diff --git a/base-demo/pom.xml b/base-demo/pom.xml index d10c5824..653b9e11 100644 --- a/base-demo/pom.xml +++ b/base-demo/pom.xml @@ -7,11 +7,11 @@ software.xdev.tci root - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT base-demo - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT jar diff --git a/base/pom.xml b/base/pom.xml index 853c63e2..847d9724 100644 --- a/base/pom.xml +++ b/base/pom.xml @@ -6,7 +6,7 @@ software.xdev.tci base - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT jar base diff --git a/bom/pom.xml b/bom/pom.xml index de9df7dc..922ea4fd 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -6,7 +6,7 @@ software.xdev.tci bom - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT pom bom @@ -51,62 +51,62 @@ software.xdev.tci base - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT software.xdev.tci db-jdbc - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT software.xdev.tci db-jdbc-spring-orm - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT software.xdev.tci db-jdbc-spring-orm-eclipselink - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT software.xdev.tci db-jdbc-spring-orm-hibernate - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT software.xdev.tci jacoco - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT software.xdev.tci jul-to-slf4j - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT software.xdev.tci junit-jupiter-api-support - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT software.xdev.tci mockserver - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT software.xdev.tci oidc-server-mock - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT software.xdev.tci selenium - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT software.xdev.tci spring-dao-support - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT diff --git a/db-jdbc-spring-orm-eclipselink/pom.xml b/db-jdbc-spring-orm-eclipselink/pom.xml index 3508dd9e..2bb1b8a5 100644 --- a/db-jdbc-spring-orm-eclipselink/pom.xml +++ b/db-jdbc-spring-orm-eclipselink/pom.xml @@ -6,7 +6,7 @@ software.xdev.tci db-jdbc-spring-orm-eclipselink - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT jar db-jdbc-spring-orm-eclipselink @@ -53,7 +53,7 @@ software.xdev.tci db-jdbc-spring-orm - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT diff --git a/db-jdbc-spring-orm-hibernate/pom.xml b/db-jdbc-spring-orm-hibernate/pom.xml index 5d61a77c..8e3cbcb2 100644 --- a/db-jdbc-spring-orm-hibernate/pom.xml +++ b/db-jdbc-spring-orm-hibernate/pom.xml @@ -6,7 +6,7 @@ software.xdev.tci db-jdbc-spring-orm-hibernate - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT jar db-jdbc-spring-orm-hibernate @@ -53,7 +53,7 @@ software.xdev.tci db-jdbc-spring-orm - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT diff --git a/db-jdbc-spring-orm/pom.xml b/db-jdbc-spring-orm/pom.xml index 30410169..e96f8c07 100644 --- a/db-jdbc-spring-orm/pom.xml +++ b/db-jdbc-spring-orm/pom.xml @@ -6,7 +6,7 @@ software.xdev.tci db-jdbc-spring-orm - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT jar db-jdbc-spring-orm @@ -53,7 +53,7 @@ software.xdev.tci db-jdbc - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT diff --git a/db-jdbc/pom.xml b/db-jdbc/pom.xml index 34598840..cf999e1a 100644 --- a/db-jdbc/pom.xml +++ b/db-jdbc/pom.xml @@ -6,7 +6,7 @@ software.xdev.tci db-jdbc - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT jar db-jdbc @@ -53,7 +53,7 @@ software.xdev.tci base - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT diff --git a/jacoco/pom.xml b/jacoco/pom.xml index 250c1adf..c095b77d 100644 --- a/jacoco/pom.xml +++ b/jacoco/pom.xml @@ -6,7 +6,7 @@ software.xdev.tci jacoco - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT jar jacoco @@ -53,7 +53,7 @@ software.xdev.tci base - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT diff --git a/jul-to-slf4j/pom.xml b/jul-to-slf4j/pom.xml index 975557c2..72eb16d7 100644 --- a/jul-to-slf4j/pom.xml +++ b/jul-to-slf4j/pom.xml @@ -6,7 +6,7 @@ software.xdev.tci jul-to-slf4j - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT jar jul-to-slf4j diff --git a/junit-jupiter-api-support/pom.xml b/junit-jupiter-api-support/pom.xml index 1d5a9864..50f3f0ff 100644 --- a/junit-jupiter-api-support/pom.xml +++ b/junit-jupiter-api-support/pom.xml @@ -6,7 +6,7 @@ software.xdev.tci junit-jupiter-api-support - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT jar junit-jupiter-api-support diff --git a/mockserver/pom.xml b/mockserver/pom.xml index 7a908c8f..07da6253 100644 --- a/mockserver/pom.xml +++ b/mockserver/pom.xml @@ -6,7 +6,7 @@ software.xdev.tci mockserver - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT jar mockserver @@ -65,7 +65,7 @@ software.xdev.tci base - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT diff --git a/oidc-server-mock/pom.xml b/oidc-server-mock/pom.xml index 25ba063d..29ac41b7 100644 --- a/oidc-server-mock/pom.xml +++ b/oidc-server-mock/pom.xml @@ -6,7 +6,7 @@ software.xdev.tci oidc-server-mock - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT jar oidc-server-mock @@ -53,7 +53,7 @@ software.xdev.tci base - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT diff --git a/pom.xml b/pom.xml index 16a6ad85..ba8dc50f 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ software.xdev.tci root - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT pom diff --git a/selenium/pom.xml b/selenium/pom.xml index 9ae5d3ca..97c5dcbb 100644 --- a/selenium/pom.xml +++ b/selenium/pom.xml @@ -6,7 +6,7 @@ software.xdev.tci selenium - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT jar selenium @@ -65,18 +65,18 @@ software.xdev.tci base - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT software.xdev.tci jul-to-slf4j - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT software.xdev.tci junit-jupiter-api-support - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT diff --git a/spring-dao-support/pom.xml b/spring-dao-support/pom.xml index a5c9d8dc..dde5baea 100644 --- a/spring-dao-support/pom.xml +++ b/spring-dao-support/pom.xml @@ -6,7 +6,7 @@ software.xdev.tci spring-dao-support - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT jar spring-dao-support @@ -53,7 +53,7 @@ software.xdev.tci db-jdbc - 3.4.2-SNAPSHOT + 3.5.0-SNAPSHOT From 0e793f47d4fac23f189c0381f946da36e6263275 Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 2 Jul 2026 14:43:14 +0200 Subject: [PATCH 016/141] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79ae1a63..738e4f50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # 3.5.0 * `db-jdbc-spring-orm` & implementations * No longer use `setPersistenceProviderClassName` because it has no effect - * By default create `PersistenceProvider` only once to make it possible to cache redundant calls + * Performance: Reuse `PersistenceProvider` # 3.4.1 * Remove dedicated code for Java 17 (no longer needed because Java 21 is the minimum) From bcac1ea4aea00f0388548d15757ed1ae5d9da989 Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 2 Jul 2026 14:46:23 +0200 Subject: [PATCH 017/141] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 738e4f50..4c407230 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ * `db-jdbc-spring-orm` & implementations * No longer use `setPersistenceProviderClassName` because it has no effect * Performance: Reuse `PersistenceProvider` +* Updated dependencies # 3.4.1 * Remove dedicated code for Java 17 (no longer needed because Java 21 is the minimum) From 0c0687409a360d900be67734c4d3da25a13b0f4e Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 2 Jul 2026 14:47:13 +0200 Subject: [PATCH 018/141] Update run-integration-tests.yml --- .github/workflows/run-integration-tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/run-integration-tests.yml b/.github/workflows/run-integration-tests.yml index 479f6bf2..102f0cf7 100644 --- a/.github/workflows/run-integration-tests.yml +++ b/.github/workflows/run-integration-tests.yml @@ -21,6 +21,7 @@ on: concurrency: group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: run-integration-tests: From 1fea56f49a65effc38c9b68a93fc1743f1458cbc Mon Sep 17 00:00:00 2001 From: XDEV Renovate Bot Date: Thu, 2 Jul 2026 12:49:56 +0000 Subject: [PATCH 019/141] Update dependency software.xdev.sse:bom to v2.5.1 --- advanced-demo/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/advanced-demo/pom.xml b/advanced-demo/pom.xml index 7f16714e..c2d8d72a 100644 --- a/advanced-demo/pom.xml +++ b/advanced-demo/pom.xml @@ -109,7 +109,7 @@ software.xdev.sse bom - 2.5.0 + 2.5.1 pom import From 49495a4e4def082b3ccb7fa50c89dbb8715eac8d Mon Sep 17 00:00:00 2001 From: "Alex \"Blex\" B" <45384811+AB-xdev@users.noreply.github.com> Date: Thu, 2 Jul 2026 14:50:26 +0200 Subject: [PATCH 020/141] Remove JaCoCo plugin Latest version is not working and we don't really need the output visible on GitHub actions --- .github/workflows/run-integration-tests.yml | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/.github/workflows/run-integration-tests.yml b/.github/workflows/run-integration-tests.yml index 102f0cf7..8a7bb3bf 100644 --- a/.github/workflows/run-integration-tests.yml +++ b/.github/workflows/run-integration-tests.yml @@ -100,23 +100,3 @@ jobs: name: webapp-jacoco-report-${{ matrix.java }}-${{ env.PROJECT_NORMALIZED }}-${{ matrix.parallel }}-${{ matrix.pre-start }} path: advanced-demo/target/site/jacoco-aggregate if-no-files-found: ignore - - - name: WebApp JaCoCo Code Coverage Report - if: ${{ matrix.jacoco && success() }} - id: jacoco_reporter - uses: PavanMudigonda/jacoco-reporter@e8b54bfea6a667d1a68624dae8a06ba31670667d # v5.1 - with: - coverage_results_path: advanced-demo/target/site/jacoco-aggregate/jacoco.xml - coverage_report_name: WebApp Coverage (${{matrix.parallel}}, ${{matrix.pre-start}}, ${{matrix.java}}) - coverage_report_title: JaCoCo - github_token: ${{ secrets.GITHUB_TOKEN }} - - - name: Add WebApp JaCoCo report to workflow run summary - if: ${{ matrix.jacoco && success() }} - run: | - echo "| Outcome | Value |" >> $GITHUB_STEP_SUMMARY - echo "| --- | --- |" >> $GITHUB_STEP_SUMMARY - echo "| Code Coverage % | ${{ steps.jacoco_reporter.outputs.coverage_percentage }} |" >> $GITHUB_STEP_SUMMARY - echo "| :heavy_check_mark: Number of Lines Covered | ${{ steps.jacoco_reporter.outputs.covered_lines }} |" >> $GITHUB_STEP_SUMMARY - echo "| :x: Number of Lines Missed | ${{ steps.jacoco_reporter.outputs.missed_lines }} |" >> $GITHUB_STEP_SUMMARY - echo "| Total Number of Lines | ${{ steps.jacoco_reporter.outputs.total_lines }} |" >> $GITHUB_STEP_SUMMARY From 83a9a665110bd1a889040ee5e94a16df6ce64ad2 Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 2 Jul 2026 14:51:56 +0200 Subject: [PATCH 021/141] Improve startup performance --- advanced-demo/webapp/src/main/resources/application.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/advanced-demo/webapp/src/main/resources/application.yml b/advanced-demo/webapp/src/main/resources/application.yml index ada5d52a..e6a4def9 100644 --- a/advanced-demo/webapp/src/main/resources/application.yml +++ b/advanced-demo/webapp/src/main/resources/application.yml @@ -1,9 +1,14 @@ spring: jpa: + bootstrap: async open-in-view: false hibernate: ddl-auto: none properties: + jakarta: + persistence: + validation: + mode: none hibernate: hbm2ddl: auto: none From a57a7c08fefc340d85d9e589f1c1c3d2a5af7de4 Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 2 Jul 2026 14:54:13 +0200 Subject: [PATCH 022/141] Update run-integration-tests.yml --- .github/workflows/run-integration-tests.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/run-integration-tests.yml b/.github/workflows/run-integration-tests.yml index 8a7bb3bf..5e1538d0 100644 --- a/.github/workflows/run-integration-tests.yml +++ b/.github/workflows/run-integration-tests.yml @@ -45,7 +45,6 @@ jobs: if: ${{ !(github.event_name == 'pull_request' && startsWith(github.head_ref, 'renovate/')) }} permissions: contents: read - checks: write # JaCoCo Coverage Report check steps: - uses: actions/checkout@v7 From 23f5093b9aee21fc1784e84dbe56cab0e10f1010 Mon Sep 17 00:00:00 2001 From: XDEV Renovate Bot Date: Sun, 5 Jul 2026 07:31:30 +0000 Subject: [PATCH 023/141] Update org.hibernate.orm to v7.4.4.Final --- db-jdbc-spring-orm-hibernate/pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/db-jdbc-spring-orm-hibernate/pom.xml b/db-jdbc-spring-orm-hibernate/pom.xml index 222312c2..050ae61c 100644 --- a/db-jdbc-spring-orm-hibernate/pom.xml +++ b/db-jdbc-spring-orm-hibernate/pom.xml @@ -59,18 +59,18 @@ org.hibernate.orm hibernate-core - 7.4.3.Final + 7.4.4.Final org.hibernate.orm hibernate-scan-jandex - 7.4.3.Final + 7.4.4.Final org.hibernate.orm hibernate-hikaricp - 7.4.3.Final + 7.4.4.Final From 9ef1b9cda4041678476fc2ca223c556f0160590a Mon Sep 17 00:00:00 2001 From: XDEV Renovate Bot Date: Mon, 6 Jul 2026 08:26:01 +0000 Subject: [PATCH 024/141] Update dependency software.xdev:testcontainers-selenium to v2.0.1 --- selenium/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selenium/pom.xml b/selenium/pom.xml index b6c4abc9..98e1c7c5 100644 --- a/selenium/pom.xml +++ b/selenium/pom.xml @@ -82,7 +82,7 @@ software.xdev testcontainers-selenium - 2.0.0 + 2.0.1 From a7b47644cef31f09cc84bcf2d48d017af0bfa840 Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 2 Jul 2026 15:29:30 +0200 Subject: [PATCH 025/141] Execute integration tests faster by building docker image natively --- .github/workflows/run-integration-tests.yml | 94 ++++++++++++++++++--- advanced-demo/webapp/.dockerignore | 5 ++ advanced-demo/webapp/Dockerfile | 11 ++- 3 files changed, 95 insertions(+), 15 deletions(-) create mode 100644 advanced-demo/webapp/.dockerignore diff --git a/.github/workflows/run-integration-tests.yml b/.github/workflows/run-integration-tests.yml index 5e1538d0..c0e8617b 100644 --- a/.github/workflows/run-integration-tests.yml +++ b/.github/workflows/run-integration-tests.yml @@ -24,25 +24,23 @@ concurrency: cancel-in-progress: true jobs: - run-integration-tests: + persistence: strategy: fail-fast: false matrix: - project: [persistence-it-hibernate, webapp-it] + project: [persistence-it-hibernate] parallel: [0, 2] pre-start: [false, true] java: [21] include: - - project: webapp-it - jacoco: true - video: true - project: persistence-it-eclipselink parallel: 2 pre-start: true java: 21 - name: "run-integration-tests (${{matrix.project}}, ${{matrix.parallel}}, ${{matrix.pre-start}}, ${{matrix.java}})" + name: "persistence (${{matrix.project}}, ${{matrix.parallel}}, ${{matrix.pre-start}}, ${{matrix.java}})" runs-on: ubuntu-latest if: ${{ !(github.event_name == 'pull_request' && startsWith(github.head_ref, 'renovate/')) }} + timeout-minutes: 10 permissions: contents: read steps: @@ -58,16 +56,84 @@ jobs: uses: actions/cache@v6 with: path: ~/.m2/repository - key: ${{ runner.os }}-mvn-it-build-${{ hashFiles('**/pom.xml') }} - restore-keys: | - ${{ runner.os }}-mvn-it-build- - ${{ runner.os }}-mvn-build- + key: ${{ runner.os }}-mvn-it-build-persistence-${{ matrix.project }}-${{ hashFiles('**/pom.xml') }} + + - name: Test + run: | + ./mvnw -B test \ + -pl "advanced-demo/integration-tests/${{ matrix.project }}" -am \ + -P run-it \ + ${{ matrix.pre-start && '-Dinfra-pre-start.enabled=1 ' || '' }} \ + ${{ matrix.parallel > 0 && format('-Djunit.jupiter.execution.parallel.enabled=true -Djunit.jupiter.execution.parallel.mode.default=concurrent -Djunit.jupiter.execution.parallel.mode.classes.default=concurrent -Djunit.jupiter.execution.parallel.config.strategy=fixed -Djunit.jupiter.execution.parallel.config.fixed.parallelism=2 -Djunit.jupiter.execution.parallel.config.fixed.max-pool-size={0} ', matrix.parallel) || '' }} + + selenium: + strategy: + fail-fast: false + matrix: + project: [webapp-it] + parallel: [0, 2] + pre-start: [false, true] + native-image-build: [true] + java: [21] + include: + - project: webapp-it + parallel: 2 + pre-start: true + native-image-build: false + java: 21 + name: "selenium (${{matrix.project}}, ${{matrix.parallel}}, ${{matrix.pre-start}}, ${{matrix.java}})" + runs-on: ubuntu-latest + if: ${{ !(github.event_name == 'pull_request' && startsWith(github.head_ref, 'renovate/')) }} + timeout-minutes: 30 + permissions: + contents: read + steps: + - uses: actions/checkout@v7 + + - name: Set up JDK + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: ${{ matrix.java }} + + - name: Cache Maven + uses: actions/cache@v6 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-mvn-it-build-selenium-${{ matrix.project }}-${{ matrix.native-image-build }}-${{ hashFiles('**/pom.xml') }} + + - name: Build webapp + if: ${{ matrix.native-image-build }} + run: | + ./mvnw -B package -pl advanced-demo/webapp -am -Pprod,dev-log -T2C -Dmaven.test.skip + + - name: Setup QEMU + if: ${{ matrix.native-image-build }} + uses: docker/setup-qemu-action@v4 + + - name: Setup buildx + if: ${{ matrix.native-image-build }} + uses: docker/setup-buildx-action@v4 + + - name: Build webapp docker image + if: ${{ matrix.native-image-build }} + uses: docker/build-push-action@v7 + with: + context: advanced-demo/webapp + build-args: | + JACOCO_AGENT_ENABLED=1 + push: false + load: true + tags: webapp + cache-from: type=gha,scope=webapp-it + cache-to: type=gha,mode=max,scope=webapp-it - name: Test run: | ./mvnw -B test \ -pl "advanced-demo/integration-tests/${{ matrix.project }}" -am \ - -P run-it${{ matrix.jacoco && ',jacoco' || '' }} \ + -P run-it,jacoco \ + ${{ matrix.native-image-build && '-DappDockerImage=webapp' || '' }} \ ${{ matrix.pre-start && '-Dinfra-pre-start.enabled=1 ' || '' }} \ ${{ matrix.parallel > 0 && format('-Djunit.jupiter.execution.parallel.enabled=true -Djunit.jupiter.execution.parallel.mode.default=concurrent -Djunit.jupiter.execution.parallel.mode.classes.default=concurrent -Djunit.jupiter.execution.parallel.config.strategy=fixed -Djunit.jupiter.execution.parallel.config.fixed.parallelism=2 -Djunit.jupiter.execution.parallel.config.fixed.max-pool-size={0} ', matrix.parallel) || '' }} @@ -79,7 +145,7 @@ jobs: run: echo PROJECT_NORMALIZED=${PROJECT/\//-} >> $GITHUB_ENV - name: Upload videos of test failures - if: ${{ matrix.video && failure() }} + if: ${{ failure() }} uses: actions/upload-artifact@v7 with: name: test-fail-videos-${{ matrix.java }}-${{ env.PROJECT_NORMALIZED }}-${{ matrix.parallel }}-${{ matrix.pre-start }} @@ -87,13 +153,13 @@ jobs: if-no-files-found: ignore - name: Aggregate WebApp JaCoCo report - if: ${{ matrix.jacoco && success() }} + if: ${{ success() }} # Important: Also run compile (if not already done) so that we have all class files - otherwise the report will be incomplete run: | ./mvnw -B compile jacoco-aggregator:report-aggregate-all -pl "advanced-demo/webapp" -am -T2C - name: Upload WebApp JaCoCo report - if: ${{ matrix.jacoco && success() }} + if: ${{ success() }} uses: actions/upload-artifact@v7 with: name: webapp-jacoco-report-${{ matrix.java }}-${{ env.PROJECT_NORMALIZED }}-${{ matrix.parallel }}-${{ matrix.pre-start }} diff --git a/advanced-demo/webapp/.dockerignore b/advanced-demo/webapp/.dockerignore new file mode 100644 index 00000000..c84ae3e6 --- /dev/null +++ b/advanced-demo/webapp/.dockerignore @@ -0,0 +1,5 @@ +# by default ignore everything! +* + +# allow only things that should be used in the context +!target/webapp.jar diff --git a/advanced-demo/webapp/Dockerfile b/advanced-demo/webapp/Dockerfile index b20b1c25..47f962f1 100644 --- a/advanced-demo/webapp/Dockerfile +++ b/advanced-demo/webapp/Dockerfile @@ -45,6 +45,9 @@ RUN java -Djarmode=tools -jar app.jar extract --layers --destination extracted FROM jre-minimized +ARG JACOCO_AGENT_ENABLED +ARG JACOCO_AGENT_VERSION="0.8.14" + ARG user=app ARG group=app ARG uid=1000 @@ -55,7 +58,13 @@ ARG APP_DIR=/opt/app RUN mkdir -p ${APP_DIR} \ && chown ${uid}:${gid} ${APP_DIR} \ && addgroup -g ${gid} ${group} \ - && adduser -h "$APP_DIR" -u ${uid} -G ${group} -s /bin/bash -D ${user} + && adduser -h "$APP_DIR" -u ${uid} -G ${group} -s /bin/bash -D ${user}; \ + # Download JaCoCoAgent if required + if [ "$JACOCO_AGENT_ENABLED" = "1" ]; then \ + mkdir -p /jacoco/report \ + && chown $uid:$gid /jacoco/report \ + && wget https://repo.maven.apache.org/maven2/org/jacoco/org.jacoco.agent/$JACOCO_AGENT_VERSION/org.jacoco.agent-$JACOCO_AGENT_VERSION-runtime.jar -O /jacoco/agent.jar; \ + fi WORKDIR ${APP_DIR} From 53984ed1aa8814073f4257c00aab8ef7384db17e Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 2 Jul 2026 15:55:40 +0200 Subject: [PATCH 026/141] New image caching approach --- .github/workflows/run-integration-tests.yml | 74 +++++++++------------ 1 file changed, 33 insertions(+), 41 deletions(-) diff --git a/.github/workflows/run-integration-tests.yml b/.github/workflows/run-integration-tests.yml index c0e8617b..4856dfd0 100644 --- a/.github/workflows/run-integration-tests.yml +++ b/.github/workflows/run-integration-tests.yml @@ -73,14 +73,7 @@ jobs: project: [webapp-it] parallel: [0, 2] pre-start: [false, true] - native-image-build: [true] java: [21] - include: - - project: webapp-it - parallel: 2 - pre-start: true - native-image-build: false - java: 21 name: "selenium (${{matrix.project}}, ${{matrix.parallel}}, ${{matrix.pre-start}}, ${{matrix.java}})" runs-on: ubuntu-latest if: ${{ !(github.event_name == 'pull_request' && startsWith(github.head_ref, 'renovate/')) }} @@ -100,55 +93,54 @@ jobs: uses: actions/cache@v6 with: path: ~/.m2/repository - key: ${{ runner.os }}-mvn-it-build-selenium-${{ matrix.project }}-${{ matrix.native-image-build }}-${{ hashFiles('**/pom.xml') }} - - - name: Build webapp - if: ${{ matrix.native-image-build }} - run: | - ./mvnw -B package -pl advanced-demo/webapp -am -Pprod,dev-log -T2C -Dmaven.test.skip + key: ${{ runner.os }}-mvn-it-build-selenium-${{ matrix.project }}-${{ hashFiles('**/pom.xml') }} - - name: Setup QEMU - if: ${{ matrix.native-image-build }} - uses: docker/setup-qemu-action@v4 - - - name: Setup buildx - if: ${{ matrix.native-image-build }} - uses: docker/setup-buildx-action@v4 - - - name: Build webapp docker image - if: ${{ matrix.native-image-build }} - uses: docker/build-push-action@v7 + - name: Restore docker image cache + uses: actions/cache/restore@v6 + id: restore-docker-image-cache with: - context: advanced-demo/webapp - build-args: | - JACOCO_AGENT_ENABLED=1 - push: false - load: true - tags: webapp - cache-from: type=gha,scope=webapp-it - cache-to: type=gha,mode=max,scope=webapp-it + path: ~/.cache/docker/ + key: ${{ runner.os }}-it-docker-images-selenium-${{ matrix.project }}-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-it-docker-images-selenium-${{ matrix.project }}-${{ hashFiles('**/pom.xml') }} + ${{ runner.os }}-it-docker-images-selenium-${{ matrix.project }}- + + - name: Load docker image cache + run: | + cat ~/.cache/docker/images.tar | docker load && echo "Restored docker image cache" && docker image ls || true - name: Test run: | ./mvnw -B test \ -pl "advanced-demo/integration-tests/${{ matrix.project }}" -am \ -P run-it,jacoco \ - ${{ matrix.native-image-build && '-DappDockerImage=webapp' || '' }} \ ${{ matrix.pre-start && '-Dinfra-pre-start.enabled=1 ' || '' }} \ ${{ matrix.parallel > 0 && format('-Djunit.jupiter.execution.parallel.enabled=true -Djunit.jupiter.execution.parallel.mode.default=concurrent -Djunit.jupiter.execution.parallel.mode.classes.default=concurrent -Djunit.jupiter.execution.parallel.config.strategy=fixed -Djunit.jupiter.execution.parallel.config.fixed.parallelism=2 -Djunit.jupiter.execution.parallel.config.fixed.max-pool-size={0} ', matrix.parallel) || '' }} - # Replace '/' with '-' - - name: Normalize project name - if: ${{ !cancelled() }} - env: - PROJECT: ${{ matrix.project }} - run: echo PROJECT_NORMALIZED=${PROJECT/\//-} >> $GITHUB_ENV + - name: Create docker image cache + run: | + echo "Images before pruning" && docker image ls + docker image prune -af --filter "until=2160h" + docker image prune -f + echo "Currently available images" && docker image ls + echo "Will NOT cache images" && docker image ls --f="label=org.testcontainers.sessionId" + mkdir -p ~/.cache/docker + image_ids_to_save=$(comm -23 <(docker images -q | sort) <(docker image ls -q --f="label=org.testcontainers.sessionId" | sort)) + echo "Saving image ids" && echo $image_ids_to_save + docker save $image_ids_to_save -o ~/.cache/docker/images.tar + ls -lha ~/.cache/docker + + - name: Save docker image cache + uses: actions/cache/restore@v6 + with: + path: ~/.cache/docker/ + key: ${{ runner.os }}-it-docker-images-selenium-${{ matrix.project }}-${{ hashFiles('**/pom.xml') }} - name: Upload videos of test failures if: ${{ failure() }} uses: actions/upload-artifact@v7 with: - name: test-fail-videos-${{ matrix.java }}-${{ env.PROJECT_NORMALIZED }}-${{ matrix.parallel }}-${{ matrix.pre-start }} + name: test-fail-videos-${{ matrix.java }}-${{ matrix.project }}-${{ matrix.parallel }}-${{ matrix.pre-start }} path: advanced-demo/integration-tests/${{ matrix.project }}/target/records if-no-files-found: ignore @@ -162,6 +154,6 @@ jobs: if: ${{ success() }} uses: actions/upload-artifact@v7 with: - name: webapp-jacoco-report-${{ matrix.java }}-${{ env.PROJECT_NORMALIZED }}-${{ matrix.parallel }}-${{ matrix.pre-start }} + name: webapp-jacoco-report-${{ matrix.java }}-${{ matrix.project }}-${{ matrix.parallel }}-${{ matrix.pre-start }} path: advanced-demo/target/site/jacoco-aggregate if-no-files-found: ignore From b337a59cc343fa1b8133cd1bc1b76dbd0ec5a046 Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 2 Jul 2026 16:03:42 +0200 Subject: [PATCH 027/141] Update run-integration-tests.yml --- .github/workflows/run-integration-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-integration-tests.yml b/.github/workflows/run-integration-tests.yml index 4856dfd0..e12958b7 100644 --- a/.github/workflows/run-integration-tests.yml +++ b/.github/workflows/run-integration-tests.yml @@ -125,7 +125,7 @@ jobs: echo "Currently available images" && docker image ls echo "Will NOT cache images" && docker image ls --f="label=org.testcontainers.sessionId" mkdir -p ~/.cache/docker - image_ids_to_save=$(comm -23 <(docker images -q | sort) <(docker image ls -q --f="label=org.testcontainers.sessionId" | sort)) + image_ids_to_save=$(comm -23 <(docker images -q | sort) <(docker image ls -q -f="label=org.testcontainers.sessionId" | sort)) echo "Saving image ids" && echo $image_ids_to_save docker save $image_ids_to_save -o ~/.cache/docker/images.tar ls -lha ~/.cache/docker From 3733d2f02b0df393333715b7af62b2f9e16c536c Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 2 Jul 2026 16:03:51 +0200 Subject: [PATCH 028/141] Update run-integration-tests.yml --- .github/workflows/run-integration-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-integration-tests.yml b/.github/workflows/run-integration-tests.yml index e12958b7..ea305f8b 100644 --- a/.github/workflows/run-integration-tests.yml +++ b/.github/workflows/run-integration-tests.yml @@ -123,7 +123,7 @@ jobs: docker image prune -af --filter "until=2160h" docker image prune -f echo "Currently available images" && docker image ls - echo "Will NOT cache images" && docker image ls --f="label=org.testcontainers.sessionId" + echo "Will NOT cache images" && docker image ls -f="label=org.testcontainers.sessionId" mkdir -p ~/.cache/docker image_ids_to_save=$(comm -23 <(docker images -q | sort) <(docker image ls -q -f="label=org.testcontainers.sessionId" | sort)) echo "Saving image ids" && echo $image_ids_to_save From 2dc1997ab430f6ca935663f72019ce14990bc23a Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 2 Jul 2026 16:06:59 +0200 Subject: [PATCH 029/141] Update run-integration-tests.yml --- .github/workflows/run-integration-tests.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/run-integration-tests.yml b/.github/workflows/run-integration-tests.yml index ea305f8b..8b07d99b 100644 --- a/.github/workflows/run-integration-tests.yml +++ b/.github/workflows/run-integration-tests.yml @@ -107,6 +107,9 @@ jobs: - name: Load docker image cache run: | + echo "Deleting preinstalled images" && docker image ls + docker image prune -af + cat ~/.cache/docker/images.tar | docker load && echo "Restored docker image cache" && docker image ls || true - name: Test @@ -123,11 +126,14 @@ jobs: docker image prune -af --filter "until=2160h" docker image prune -f echo "Currently available images" && docker image ls + echo "Will NOT cache images" && docker image ls -f="label=org.testcontainers.sessionId" + mkdir -p ~/.cache/docker image_ids_to_save=$(comm -23 <(docker images -q | sort) <(docker image ls -q -f="label=org.testcontainers.sessionId" | sort)) echo "Saving image ids" && echo $image_ids_to_save docker save $image_ids_to_save -o ~/.cache/docker/images.tar + ls -lha ~/.cache/docker - name: Save docker image cache From 91dd68a82c04665a3210f0b4104c408d7941316f Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 2 Jul 2026 16:22:17 +0200 Subject: [PATCH 030/141] Update run-integration-tests.yml --- .github/workflows/run-integration-tests.yml | 41 ++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/.github/workflows/run-integration-tests.yml b/.github/workflows/run-integration-tests.yml index 8b07d99b..b8abcea8 100644 --- a/.github/workflows/run-integration-tests.yml +++ b/.github/workflows/run-integration-tests.yml @@ -58,6 +58,23 @@ jobs: path: ~/.m2/repository key: ${{ runner.os }}-mvn-it-build-persistence-${{ matrix.project }}-${{ hashFiles('**/pom.xml') }} + - name: Restore docker image cache + uses: actions/cache/restore@v6 + id: restore-docker-image-cache + with: + path: ~/.cache/docker/ + key: ${{ runner.os }}-it-docker-images-persistence-${{ matrix.project }}-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-it-docker-images-persistence-${{ matrix.project }}-${{ hashFiles('**/pom.xml') }} + ${{ runner.os }}-it-docker-images-persistence-${{ matrix.project }}- + + - name: Load docker image cache + run: | + echo "Deleting preinstalled images" && docker image ls + docker image prune -af + + cat ~/.cache/docker/images.tar | docker load && echo "Restored docker image cache" && docker image ls || true + - name: Test run: | ./mvnw -B test \ @@ -66,6 +83,28 @@ jobs: ${{ matrix.pre-start && '-Dinfra-pre-start.enabled=1 ' || '' }} \ ${{ matrix.parallel > 0 && format('-Djunit.jupiter.execution.parallel.enabled=true -Djunit.jupiter.execution.parallel.mode.default=concurrent -Djunit.jupiter.execution.parallel.mode.classes.default=concurrent -Djunit.jupiter.execution.parallel.config.strategy=fixed -Djunit.jupiter.execution.parallel.config.fixed.parallelism=2 -Djunit.jupiter.execution.parallel.config.fixed.max-pool-size={0} ', matrix.parallel) || '' }} + - name: Create docker image cache + run: | + echo "Images before pruning" && docker image ls + docker image prune -af --filter "until=2160h" + docker image prune -f + echo "Currently available images" && docker image ls + + echo "Will NOT cache images" && docker image ls -f="label=org.testcontainers.sessionId" + + mkdir -p ~/.cache/docker + image_ids_to_save=$(comm -23 <(docker images -q | sort) <(docker image ls -q -f="label=org.testcontainers.sessionId" | sort)) + echo "Saving image ids" && echo $image_ids_to_save + docker save $image_ids_to_save -o ~/.cache/docker/images.tar + + ls -lha ~/.cache/docker + + - name: Save docker image cache + uses: actions/cache/save@v6 + with: + path: ~/.cache/docker/ + key: ${{ runner.os }}-it-docker-images-persistence-${{ matrix.project }}-${{ hashFiles('**/pom.xml') }} + selenium: strategy: fail-fast: false @@ -137,7 +176,7 @@ jobs: ls -lha ~/.cache/docker - name: Save docker image cache - uses: actions/cache/restore@v6 + uses: actions/cache/save@v6 with: path: ~/.cache/docker/ key: ${{ runner.os }}-it-docker-images-selenium-${{ matrix.project }}-${{ hashFiles('**/pom.xml') }} From 80057e0b599179dbec4270926bb36fa4095bff25 Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 2 Jul 2026 16:30:35 +0200 Subject: [PATCH 031/141] Update run-integration-tests.yml --- .github/workflows/run-integration-tests.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/run-integration-tests.yml b/.github/workflows/run-integration-tests.yml index b8abcea8..30e16bf3 100644 --- a/.github/workflows/run-integration-tests.yml +++ b/.github/workflows/run-integration-tests.yml @@ -84,6 +84,8 @@ jobs: ${{ matrix.parallel > 0 && format('-Djunit.jupiter.execution.parallel.enabled=true -Djunit.jupiter.execution.parallel.mode.default=concurrent -Djunit.jupiter.execution.parallel.mode.classes.default=concurrent -Djunit.jupiter.execution.parallel.config.strategy=fixed -Djunit.jupiter.execution.parallel.config.fixed.parallelism=2 -Djunit.jupiter.execution.parallel.config.fixed.max-pool-size={0} ', matrix.parallel) || '' }} - name: Create docker image cache + # Cache can't be saved when it's already present + if: steps.cache.outputs.cache-hit != 'true' run: | echo "Images before pruning" && docker image ls docker image prune -af --filter "until=2160h" @@ -100,6 +102,7 @@ jobs: ls -lha ~/.cache/docker - name: Save docker image cache + if: steps.cache.outputs.cache-hit != 'true' uses: actions/cache/save@v6 with: path: ~/.cache/docker/ @@ -160,6 +163,8 @@ jobs: ${{ matrix.parallel > 0 && format('-Djunit.jupiter.execution.parallel.enabled=true -Djunit.jupiter.execution.parallel.mode.default=concurrent -Djunit.jupiter.execution.parallel.mode.classes.default=concurrent -Djunit.jupiter.execution.parallel.config.strategy=fixed -Djunit.jupiter.execution.parallel.config.fixed.parallelism=2 -Djunit.jupiter.execution.parallel.config.fixed.max-pool-size={0} ', matrix.parallel) || '' }} - name: Create docker image cache + # Cache can't be saved when it's already present + if: steps.cache.outputs.cache-hit != 'true' run: | echo "Images before pruning" && docker image ls docker image prune -af --filter "until=2160h" @@ -176,6 +181,7 @@ jobs: ls -lha ~/.cache/docker - name: Save docker image cache + if: steps.cache.outputs.cache-hit != 'true' uses: actions/cache/save@v6 with: path: ~/.cache/docker/ From 01bed2fe563dd57f5acb5b83ed4a8475af2ade56 Mon Sep 17 00:00:00 2001 From: AB Date: Fri, 3 Jul 2026 13:23:57 +0200 Subject: [PATCH 032/141] Tag intermediate builds --- .github/workflows/run-integration-tests.yml | 1 + advanced-demo/integration-tests/pom.xml | 2 +- .../containers/WebAppContainerBuilder.java | 149 ++++++++++-------- .../tci/webapp/factory/WebAppTCIFactory.java | 34 ++-- .../xdev/tci/concurrent/Suppliers.java | 112 +++++++++++++ selenium/pom.xml | 2 +- 6 files changed, 213 insertions(+), 87 deletions(-) create mode 100644 base/src/main/java/software/xdev/tci/concurrent/Suppliers.java diff --git a/.github/workflows/run-integration-tests.yml b/.github/workflows/run-integration-tests.yml index 30e16bf3..76529549 100644 --- a/.github/workflows/run-integration-tests.yml +++ b/.github/workflows/run-integration-tests.yml @@ -159,6 +159,7 @@ jobs: ./mvnw -B test \ -pl "advanced-demo/integration-tests/${{ matrix.project }}" -am \ -P run-it,jacoco \ + -DtagIntermediate=1 \ ${{ matrix.pre-start && '-Dinfra-pre-start.enabled=1 ' || '' }} \ ${{ matrix.parallel > 0 && format('-Djunit.jupiter.execution.parallel.enabled=true -Djunit.jupiter.execution.parallel.mode.default=concurrent -Djunit.jupiter.execution.parallel.mode.classes.default=concurrent -Djunit.jupiter.execution.parallel.config.strategy=fixed -Djunit.jupiter.execution.parallel.config.fixed.parallelism=2 -Djunit.jupiter.execution.parallel.config.fixed.max-pool-size={0} ', matrix.parallel) || '' }} diff --git a/advanced-demo/integration-tests/pom.xml b/advanced-demo/integration-tests/pom.xml index 2a891042..cea820dc 100644 --- a/advanced-demo/integration-tests/pom.xml +++ b/advanced-demo/integration-tests/pom.xml @@ -118,7 +118,7 @@ software.xdev testcontainers-advanced-imagebuilder - 2.5.0 + 3.0.1 diff --git a/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/containers/WebAppContainerBuilder.java b/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/containers/WebAppContainerBuilder.java index e6bffafb..2cee6c56 100644 --- a/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/containers/WebAppContainerBuilder.java +++ b/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/containers/WebAppContainerBuilder.java @@ -3,14 +3,15 @@ import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; +import java.time.Duration; import java.util.List; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; +import java.util.concurrent.CompletableFuture; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import software.xdev.tci.concurrent.TCIExecutorServiceHolder; import software.xdev.tci.jacoco.testbase.config.JaCoCoConfig; import software.xdev.testcontainers.imagebuilder.AdvancedImageFromDockerFile; import software.xdev.testcontainers.imagebuilder.compat.DockerfileCOPYParentsEmulator; @@ -30,7 +31,7 @@ private WebAppContainerBuilder() { } - public static synchronized String getBuiltImageName() + public static synchronized String getBuiltImageName(final boolean tagIntermediate) { if(builtImageName != null) { @@ -42,81 +43,97 @@ public static synchronized String getBuiltImageName() final AdvancedImageFromDockerFile builder = new AdvancedImageFromDockerFile("webapp-it-local", false) .withLoggerForBuild(LOG_CONTAINER_BUILD) - .withPostGitIgnoreLines( - // Ignore git-folder, as it will be provided in the Dockerfile - ".git/**", - // Ignore other unused folders and extensions - "*.iml", - "*.cmd", - "*.md", - "_dev_infra/**", - "_resource_metrics/**", - // Ignore other Dockerfiles (our required file will always be transferred) - "Dockerfile", - // Ignore not required test-modules that may have changed - // sources only - otherwise the parent pom doesn't find the resources - "integration-tests/**", - "**/src/test/**", - // Ignore resources that are just used for development - "webapp/src/main/resources-dev/**", - // Most files from these folders need to be ignored -> Down there for highest prio - "node_modules", - "target") .withDockerFilePath(Paths.get("../../integration-tests/tci-webapp/Dockerfile")) .withBaseDir(Paths.get("../../")) - // File is in root directory - we can't access it - .withBaseDirRelativeIgnoreFile(null) - .withDockerFileLinesModifier(new DockerfileCOPYParentsEmulator()) - .withTransferArchiveTARCompressorCustomizer(c -> c - // Rewrite parent pom to exclude integration tests - // This way changes in test pom's cause no redownload of dependencies - .withContentModifier(new FileLinesContentModifier() - { - @Override - public boolean shouldApply( - final Path sourcePath, - final String targetPath, - final TarArchiveEntry tarArchiveEntry) - { - return "pom.xml".equals(targetPath); - } - - @Override - public List modify( - final List lines, - final Path sourcePath, - final String targetPath, - final TarArchiveEntry tarArchiveEntry) throws IOException - { - return lines.stream() - // Remove integration tests module - .filter(s -> !s.contains("integration-tests")) - .toList(); - } - - @Override - public boolean isIdentical(final List original, final List created) + .withCreateTransferFilesCache(tagIntermediate) + .configureFilesToTransferHandler(h -> h + .withPostGitIgnoreLines( + // Ignore git-folder, as it will be provided in the Dockerfile + ".git/**", + // Ignore other unused folders and extensions + "*.iml", + "*.cmd", + "*.md", + "_dev_infra/**", + "_resource_metrics/**", + // Ignore other Dockerfiles (our required file will always be transferred) + "Dockerfile", + // Ignore not required test-modules that may have changed + // sources only - otherwise the parent pom doesn't find the resources + "integration-tests/**", + "**/src/test/**", + // Ignore resources that are just used for development + "webapp/src/main/resources-dev/**", + // Most files from these folders need to be ignored -> Down there for highest prio + "node_modules", + "target") + // File is in root directory - we can't access it + .withBaseDirRelativeIgnoreFile(null) + .withDockerFileLinesModifier(new DockerfileCOPYParentsEmulator()) + .withTransferArchiveTARCompressorCustomizer(c -> c + // Rewrite parent pom to exclude integration tests + // This way changes in test pom's cause no redownload of dependencies + .withContentModifier(new FileLinesContentModifier() { - return original.size() == created.size(); - } - })); + @Override + public boolean shouldApply( + final Path sourcePath, + final String targetPath, + final TarArchiveEntry tarArchiveEntry) + { + return "pom.xml".equals(targetPath); + } + + @Override + public List modify( + final List lines, + final Path sourcePath, + final String targetPath, + final TarArchiveEntry tarArchiveEntry) throws IOException + { + return lines.stream() + // Remove integration tests module + .filter(s -> !s.contains("integration-tests")) + .toList(); + } + + @Override + public boolean isIdentical(final List original, final List created) + { + return original.size() == created.size(); + } + })) + ); if(JaCoCoConfig.instance().enabled()) { builder.withBuildArg("JACOCO_AGENT_ENABLED", "1"); } - try - { - builtImageName = builder.get(5, TimeUnit.MINUTES); - } - catch(final TimeoutException tex) - { - throw new IllegalStateException("Timed out", tex); - } + builtImageName = builder.build(Duration.ofMinutes(5)); LOG.info("Built Image; Name ='{}'", builtImageName); + if(tagIntermediate) + { + CompletableFuture.runAsync( + () -> { + try + { + builder.copyForIntermediateTag("webapp-it-local-builder", "builder") + .build(Duration.ofMinutes(1)); + builder.cleanCreatedTransferFilesCache(); + + LOG.info("Tagged intermediate image"); + } + catch(final Exception ex) + { + LOG.warn("Tagging intermediate target failed", ex); + } + }, + TCIExecutorServiceHolder.instance()); + } + return builtImageName; } } diff --git a/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/factory/WebAppTCIFactory.java b/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/factory/WebAppTCIFactory.java index 05498a62..39f7cab2 100644 --- a/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/factory/WebAppTCIFactory.java +++ b/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/factory/WebAppTCIFactory.java @@ -1,8 +1,11 @@ package software.xdev.tci.demo.tci.webapp.factory; import java.time.Duration; +import java.util.Objects; import java.util.function.Consumer; +import java.util.function.Supplier; +import software.xdev.tci.concurrent.Suppliers; import software.xdev.tci.demo.tci.webapp.WebAppTCI; import software.xdev.tci.demo.tci.webapp.containers.WebAppContainer; import software.xdev.tci.demo.tci.webapp.containers.WebAppContainerBuilder; @@ -14,8 +17,17 @@ public class WebAppTCIFactory extends PreStartableTCIFactory { public static final String PROPERTY_APP_DOCKERIMAGE = "appDockerImage"; + public static final String PROPERTY_TAG_INTERMEDIATE = "tagIntermediate"; - protected static String appImageName; + protected static final Supplier APP_IMAGE_NAME_SUPPLIER = Suppliers.memoize(() -> + Objects.requireNonNullElseGet( + System.getProperty(PROPERTY_APP_DOCKERIMAGE), + () -> { + String property = System.getProperty(PROPERTY_TAG_INTERMEDIATE); + return WebAppContainerBuilder.getBuiltImageName( + "1".equals(property) || Boolean.parseBoolean(property)); + }) + ); @SuppressWarnings("checkstyle:MagicNumber") public WebAppTCIFactory(final Consumer additionalContainerBuilder) @@ -23,7 +35,7 @@ public WebAppTCIFactory(final Consumer additionalContainerBuild super( WebAppTCI::new, () -> { - final WebAppContainer container = new WebAppContainer(getAppImageName(), true) + final WebAppContainer container = new WebAppContainer(APP_IMAGE_NAME_SUPPLIER.get(), true) .withDefaultWaitStrategy( Duration.ofSeconds(40L + 20L * EnvironmentPerformance.cpuSlownessFactor()), WebAppTCI.ACTUATOR_USERNAME, @@ -47,23 +59,7 @@ public WebAppTCIFactory(final Consumer additionalContainerBuild @Override protected void warmUpInternal() { - getAppImageName(); + APP_IMAGE_NAME_SUPPLIER.get(); super.warmUpInternal(); } - - protected static synchronized String getAppImageName() - { - if(appImageName != null) - { - return appImageName; - } - - appImageName = System.getProperty(PROPERTY_APP_DOCKERIMAGE); - if(appImageName == null) - { - appImageName = WebAppContainerBuilder.getBuiltImageName(); - } - - return appImageName; - } } diff --git a/base/src/main/java/software/xdev/tci/concurrent/Suppliers.java b/base/src/main/java/software/xdev/tci/concurrent/Suppliers.java new file mode 100644 index 00000000..9c5df503 --- /dev/null +++ b/base/src/main/java/software/xdev/tci/concurrent/Suppliers.java @@ -0,0 +1,112 @@ +/* + * Copyright © 2024 XDEV Software (https://xdev.software) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package software.xdev.tci.concurrent; + +import java.util.Objects; +import java.util.function.Supplier; + + +/** + * Based on + * + * Guava Supplier + * + * and minimized. + */ +public final class Suppliers +{ + private Suppliers() + { + } + + /** + * Returns a supplier which caches the instance retrieved during the first call to {@code get()} and returns that + * value on subsequent calls to {@code get()}. See: memoization + * + *

The returned supplier is thread-safe. The delegate's {@code get()} method will be invoked at + * most once unless the underlying {@code get()} throws an exception. The supplier's serialized form does not + * contain the cached value, which will be recalculated when {@code get()} is called on the deserialized instance. + * + *

When the underlying delegate throws an exception then this memoizing supplier will keep + * delegating calls until it returns valid data. + * + *

If {@code delegate} is an instance created by an earlier call to {@code memoize}, it is + * returned directly. + */ + public static Supplier memoize(final Supplier delegate) + { + if(delegate instanceof NonSerializableMemoizingSupplier) + { + return delegate; + } + return new NonSerializableMemoizingSupplier<>(delegate); + } + + @SuppressWarnings("java:S3077") + static class NonSerializableMemoizingSupplier implements Supplier + { + private final Object lock = new Object(); + + @SuppressWarnings("UnnecessaryLambda") // Must be a fixed singleton object + private static final Supplier SUCCESSFULLY_COMPUTED = + () -> { + throw new IllegalStateException(); // Should never get called. + }; + + private volatile Supplier delegate; + // "value" does not need to be volatile; visibility piggy-backs on volatile read of "delegate". + private T value; + + NonSerializableMemoizingSupplier(final Supplier delegate) + { + this.delegate = Objects.requireNonNull(delegate); + } + + @Override + @SuppressWarnings("unchecked") // Cast from Supplier to Supplier is always valid + public T get() + { + // Because Supplier is read-heavy, we use the "double-checked locking" pattern. + if(this.delegate != SUCCESSFULLY_COMPUTED) + { + synchronized(this.lock) + { + if(this.delegate != SUCCESSFULLY_COMPUTED) + { + final T t = this.delegate.get(); + this.value = t; + this.delegate = (Supplier)SUCCESSFULLY_COMPUTED; + return t; + } + } + } + // This is safe because we checked `delegate`. + return this.value; + } + + @Override + public String toString() + { + final Supplier currentDelegate = this.delegate; + return "Suppliers.memoize(" + + (currentDelegate == SUCCESSFULLY_COMPUTED + ? "" + : currentDelegate) + + ")"; + } + } +} diff --git a/selenium/pom.xml b/selenium/pom.xml index b6c4abc9..98e1c7c5 100644 --- a/selenium/pom.xml +++ b/selenium/pom.xml @@ -82,7 +82,7 @@ software.xdev testcontainers-selenium - 2.0.0 + 2.0.1 From b7c66e7a90483e5469d7aef11e9050b466102e4c Mon Sep 17 00:00:00 2001 From: AB Date: Fri, 3 Jul 2026 13:38:45 +0200 Subject: [PATCH 033/141] Fix PMD problems --- .../java/software/xdev/tci/concurrent/Suppliers.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/base/src/main/java/software/xdev/tci/concurrent/Suppliers.java b/base/src/main/java/software/xdev/tci/concurrent/Suppliers.java index 9c5df503..c1780e63 100644 --- a/base/src/main/java/software/xdev/tci/concurrent/Suppliers.java +++ b/base/src/main/java/software/xdev/tci/concurrent/Suppliers.java @@ -16,6 +16,7 @@ package software.xdev.tci.concurrent; import java.util.Objects; +import java.util.concurrent.locks.ReentrantLock; import java.util.function.Supplier; @@ -59,7 +60,7 @@ public static Supplier memoize(final Supplier delegate) @SuppressWarnings("java:S3077") static class NonSerializableMemoizingSupplier implements Supplier { - private final Object lock = new Object(); + private final ReentrantLock lock = new ReentrantLock(); @SuppressWarnings("UnnecessaryLambda") // Must be a fixed singleton object private static final Supplier SUCCESSFULLY_COMPUTED = @@ -67,6 +68,7 @@ static class NonSerializableMemoizingSupplier implements Supplier throw new IllegalStateException(); // Should never get called. }; + @SuppressWarnings("PMD.AvoidUsingVolatile") private volatile Supplier delegate; // "value" does not need to be volatile; visibility piggy-backs on volatile read of "delegate". private T value; @@ -83,7 +85,8 @@ public T get() // Because Supplier is read-heavy, we use the "double-checked locking" pattern. if(this.delegate != SUCCESSFULLY_COMPUTED) { - synchronized(this.lock) + this.lock.lock(); + try { if(this.delegate != SUCCESSFULLY_COMPUTED) { @@ -93,6 +96,10 @@ public T get() return t; } } + finally + { + this.lock.unlock(); + } } // This is safe because we checked `delegate`. return this.value; From 88d4b39cf48f286e6402b5c203dd33d07cde972d Mon Sep 17 00:00:00 2001 From: AB Date: Fri, 3 Jul 2026 13:54:24 +0200 Subject: [PATCH 034/141] Cleanup code use memoizing supplier --- .../tci/db/containers/DBContainerBuilder.java | 34 ++++++------------- .../tci/demo/tci/db/factory/DBTCIFactory.java | 9 +++-- .../containers/WebAppContainerBuilder.java | 15 +++----- .../tci/webapp/factory/WebAppTCIFactory.java | 8 ++--- 4 files changed, 26 insertions(+), 40 deletions(-) diff --git a/advanced-demo/integration-tests/tci-db/src/main/java/software/xdev/tci/demo/tci/db/containers/DBContainerBuilder.java b/advanced-demo/integration-tests/tci-db/src/main/java/software/xdev/tci/demo/tci/db/containers/DBContainerBuilder.java index 00545b95..c8e66909 100644 --- a/advanced-demo/integration-tests/tci-db/src/main/java/software/xdev/tci/demo/tci/db/containers/DBContainerBuilder.java +++ b/advanced-demo/integration-tests/tci-db/src/main/java/software/xdev/tci/demo/tci/db/containers/DBContainerBuilder.java @@ -1,8 +1,7 @@ package software.xdev.tci.demo.tci.db.containers; import java.nio.file.Paths; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; +import java.time.Duration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -17,42 +16,31 @@ public final class DBContainerBuilder private static final Logger LOG_CONTAINER_BUILD = LoggerFactory.getLogger("container.build.db"); - private static String builtImageName; private DBContainerBuilder() { } - public static synchronized String getBuiltImageName() + public static synchronized String getImageName() { - if(builtImageName != null) - { - return builtImageName; - } - LOG.info("Building Webapp-db-DockerImage..."); final AdvancedImageFromDockerFile builder = new AdvancedImageFromDockerFile("webapp-db", false) .withLoggerForBuild(LOG_CONTAINER_BUILD) - .withPostGitIgnoreLines( - // Ignore everything - "**") .withDockerFilePath(Paths.get("../tci-db/Dockerfile")) .withBaseDir(Paths.get("../")) - .withBaseDirRelativeIgnoreFile(null); + .configureFilesToTransferHandler(h -> h + .withPostGitIgnoreLines( + // Ignore everything + "**") + .withBaseDirRelativeIgnoreFile(null) + ); - try - { - builtImageName = builder.get(5, TimeUnit.MINUTES); - } - catch(final TimeoutException tex) - { - throw new IllegalStateException("Timed out", tex); - } + final String name = builder.build(Duration.ofMinutes(5)); - LOG.info("Built Image; Name ='{}'", builtImageName); + LOG.info("Built Image; Name ='{}'", name); - return builtImageName; + return name; } } diff --git a/advanced-demo/integration-tests/tci-db/src/main/java/software/xdev/tci/demo/tci/db/factory/DBTCIFactory.java b/advanced-demo/integration-tests/tci-db/src/main/java/software/xdev/tci/demo/tci/db/factory/DBTCIFactory.java index c4a0ca85..29a9dc15 100644 --- a/advanced-demo/integration-tests/tci-db/src/main/java/software/xdev/tci/demo/tci/db/factory/DBTCIFactory.java +++ b/advanced-demo/integration-tests/tci-db/src/main/java/software/xdev/tci/demo/tci/db/factory/DBTCIFactory.java @@ -1,5 +1,8 @@ package software.xdev.tci.demo.tci.db.factory; +import java.util.function.Supplier; + +import software.xdev.tci.concurrent.Suppliers; import software.xdev.tci.db.factory.BaseDBTCIFactory; import software.xdev.tci.demo.tci.db.DBTCI; import software.xdev.tci.demo.tci.db.containers.DBContainer; @@ -10,6 +13,8 @@ public class DBTCIFactory extends BaseDBTCIFactory { + protected static final Supplier IMAGE_NAME_SUPPLIER = Suppliers.memoize(DBContainerBuilder::getImageName); + public DBTCIFactory() { this(true); @@ -20,7 +25,7 @@ public DBTCIFactory(final boolean migrateAndInitializeEMC) { super( (c, n) -> new DBTCI(c, n, migrateAndInitializeEMC), - () -> new DBContainer(DBContainerBuilder.getBuiltImageName()) + () -> new DBContainer(DBContainerBuilder.getImageName()) .withDatabaseName(DBTCI.DB_DATABASE) .withCreateContainerCmdModifier(cmd -> cmd.getHostConfig().withMemory(ContainerMemory.M512M))); this.withSnapshotManager(new CommitedImageSnapshotManager("/var/lib/mysql")); @@ -29,7 +34,7 @@ public DBTCIFactory(final boolean migrateAndInitializeEMC) @Override protected void warmUpInternal() { - DBContainerBuilder.getBuiltImageName(); + DBContainerBuilder.getImageName(); super.warmUpInternal(); } } diff --git a/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/containers/WebAppContainerBuilder.java b/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/containers/WebAppContainerBuilder.java index 2cee6c56..bbea26c7 100644 --- a/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/containers/WebAppContainerBuilder.java +++ b/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/containers/WebAppContainerBuilder.java @@ -25,19 +25,12 @@ public final class WebAppContainerBuilder private static final Logger LOG_CONTAINER_BUILD = LoggerFactory.getLogger("container.build.webapp"); - private static String builtImageName; - private WebAppContainerBuilder() { } - public static synchronized String getBuiltImageName(final boolean tagIntermediate) + public static String getImageName(final boolean tagIntermediate) { - if(builtImageName != null) - { - return builtImageName; - } - LOG.info("Building WebApp-DockerImage..."); final AdvancedImageFromDockerFile builder = @@ -110,9 +103,9 @@ public boolean isIdentical(final List original, final List creat builder.withBuildArg("JACOCO_AGENT_ENABLED", "1"); } - builtImageName = builder.build(Duration.ofMinutes(5)); + final String imageName = builder.build(Duration.ofMinutes(5)); - LOG.info("Built Image; Name ='{}'", builtImageName); + LOG.info("Built Image; Name ='{}'", imageName); if(tagIntermediate) { @@ -134,6 +127,6 @@ public boolean isIdentical(final List original, final List creat TCIExecutorServiceHolder.instance()); } - return builtImageName; + return imageName; } } diff --git a/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/factory/WebAppTCIFactory.java b/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/factory/WebAppTCIFactory.java index 39f7cab2..5a36772a 100644 --- a/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/factory/WebAppTCIFactory.java +++ b/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/factory/WebAppTCIFactory.java @@ -19,12 +19,12 @@ public class WebAppTCIFactory extends PreStartableTCIFactory APP_IMAGE_NAME_SUPPLIER = Suppliers.memoize(() -> + protected static final Supplier IMAGE_NAME_SUPPLIER = Suppliers.memoize(() -> Objects.requireNonNullElseGet( System.getProperty(PROPERTY_APP_DOCKERIMAGE), () -> { String property = System.getProperty(PROPERTY_TAG_INTERMEDIATE); - return WebAppContainerBuilder.getBuiltImageName( + return WebAppContainerBuilder.getImageName( "1".equals(property) || Boolean.parseBoolean(property)); }) ); @@ -35,7 +35,7 @@ public WebAppTCIFactory(final Consumer additionalContainerBuild super( WebAppTCI::new, () -> { - final WebAppContainer container = new WebAppContainer(APP_IMAGE_NAME_SUPPLIER.get(), true) + final WebAppContainer container = new WebAppContainer(IMAGE_NAME_SUPPLIER.get(), true) .withDefaultWaitStrategy( Duration.ofSeconds(40L + 20L * EnvironmentPerformance.cpuSlownessFactor()), WebAppTCI.ACTUATOR_USERNAME, @@ -59,7 +59,7 @@ public WebAppTCIFactory(final Consumer additionalContainerBuild @Override protected void warmUpInternal() { - APP_IMAGE_NAME_SUPPLIER.get(); + IMAGE_NAME_SUPPLIER.get(); super.warmUpInternal(); } } From b3d3b29c761a81ad9fadacb7ad51e041adbd4249 Mon Sep 17 00:00:00 2001 From: AB Date: Fri, 3 Jul 2026 13:55:46 +0200 Subject: [PATCH 035/141] DEBUG --- .github/workflows/run-integration-tests.yml | 167 ++++++++++---------- advanced-demo/pom.xml | 2 + 2 files changed, 85 insertions(+), 84 deletions(-) diff --git a/.github/workflows/run-integration-tests.yml b/.github/workflows/run-integration-tests.yml index 76529549..d6bb3375 100644 --- a/.github/workflows/run-integration-tests.yml +++ b/.github/workflows/run-integration-tests.yml @@ -24,89 +24,89 @@ concurrency: cancel-in-progress: true jobs: - persistence: - strategy: - fail-fast: false - matrix: - project: [persistence-it-hibernate] - parallel: [0, 2] - pre-start: [false, true] - java: [21] - include: - - project: persistence-it-eclipselink - parallel: 2 - pre-start: true - java: 21 - name: "persistence (${{matrix.project}}, ${{matrix.parallel}}, ${{matrix.pre-start}}, ${{matrix.java}})" - runs-on: ubuntu-latest - if: ${{ !(github.event_name == 'pull_request' && startsWith(github.head_ref, 'renovate/')) }} - timeout-minutes: 10 - permissions: - contents: read - steps: - - uses: actions/checkout@v7 - - - name: Set up JDK - uses: actions/setup-java@v5 - with: - distribution: temurin - java-version: ${{ matrix.java }} - - - name: Cache Maven - uses: actions/cache@v6 - with: - path: ~/.m2/repository - key: ${{ runner.os }}-mvn-it-build-persistence-${{ matrix.project }}-${{ hashFiles('**/pom.xml') }} - - - name: Restore docker image cache - uses: actions/cache/restore@v6 - id: restore-docker-image-cache - with: - path: ~/.cache/docker/ - key: ${{ runner.os }}-it-docker-images-persistence-${{ matrix.project }}-${{ hashFiles('**/pom.xml') }} - restore-keys: | - ${{ runner.os }}-it-docker-images-persistence-${{ matrix.project }}-${{ hashFiles('**/pom.xml') }} - ${{ runner.os }}-it-docker-images-persistence-${{ matrix.project }}- - - - name: Load docker image cache - run: | - echo "Deleting preinstalled images" && docker image ls - docker image prune -af - - cat ~/.cache/docker/images.tar | docker load && echo "Restored docker image cache" && docker image ls || true - - - name: Test - run: | - ./mvnw -B test \ - -pl "advanced-demo/integration-tests/${{ matrix.project }}" -am \ - -P run-it \ - ${{ matrix.pre-start && '-Dinfra-pre-start.enabled=1 ' || '' }} \ - ${{ matrix.parallel > 0 && format('-Djunit.jupiter.execution.parallel.enabled=true -Djunit.jupiter.execution.parallel.mode.default=concurrent -Djunit.jupiter.execution.parallel.mode.classes.default=concurrent -Djunit.jupiter.execution.parallel.config.strategy=fixed -Djunit.jupiter.execution.parallel.config.fixed.parallelism=2 -Djunit.jupiter.execution.parallel.config.fixed.max-pool-size={0} ', matrix.parallel) || '' }} - - - name: Create docker image cache - # Cache can't be saved when it's already present - if: steps.cache.outputs.cache-hit != 'true' - run: | - echo "Images before pruning" && docker image ls - docker image prune -af --filter "until=2160h" - docker image prune -f - echo "Currently available images" && docker image ls - - echo "Will NOT cache images" && docker image ls -f="label=org.testcontainers.sessionId" - - mkdir -p ~/.cache/docker - image_ids_to_save=$(comm -23 <(docker images -q | sort) <(docker image ls -q -f="label=org.testcontainers.sessionId" | sort)) - echo "Saving image ids" && echo $image_ids_to_save - docker save $image_ids_to_save -o ~/.cache/docker/images.tar - - ls -lha ~/.cache/docker - - - name: Save docker image cache - if: steps.cache.outputs.cache-hit != 'true' - uses: actions/cache/save@v6 - with: - path: ~/.cache/docker/ - key: ${{ runner.os }}-it-docker-images-persistence-${{ matrix.project }}-${{ hashFiles('**/pom.xml') }} + # persistence: + # strategy: + # fail-fast: false + # matrix: + # project: [persistence-it-hibernate] + # parallel: [0, 2] + # pre-start: [false, true] + # java: [21] + # include: + # - project: persistence-it-eclipselink + # parallel: 2 + # pre-start: true + # java: 21 + # name: "persistence (${{matrix.project}}, ${{matrix.parallel}}, ${{matrix.pre-start}}, ${{matrix.java}})" + # runs-on: ubuntu-latest + # if: ${{ !(github.event_name == 'pull_request' && startsWith(github.head_ref, 'renovate/')) }} + # timeout-minutes: 10 + # permissions: + # contents: read + # steps: + # - uses: actions/checkout@v7 + + # - name: Set up JDK + # uses: actions/setup-java@v5 + # with: + # distribution: temurin + # java-version: ${{ matrix.java }} + + # - name: Cache Maven + # uses: actions/cache@v6 + # with: + # path: ~/.m2/repository + # key: ${{ runner.os }}-mvn-it-build-persistence-${{ matrix.project }}-${{ hashFiles('**/pom.xml') }} + + # - name: Restore docker image cache + # uses: actions/cache/restore@v6 + # id: restore-docker-image-cache + # with: + # path: ~/.cache/docker/ + # key: ${{ runner.os }}-it-docker-images-persistence-${{ matrix.project }}-${{ hashFiles('**/pom.xml') }} + # restore-keys: | + # ${{ runner.os }}-it-docker-images-persistence-${{ matrix.project }}-${{ hashFiles('**/pom.xml') }} + # ${{ runner.os }}-it-docker-images-persistence-${{ matrix.project }}- + + # - name: Load docker image cache + # run: | + # echo "Deleting preinstalled images" && docker image ls + # docker image prune -af + + # cat ~/.cache/docker/images.tar | docker load && echo "Restored docker image cache" && docker image ls || true + + # - name: Test + # run: | + # ./mvnw -B test \ + # -pl "advanced-demo/integration-tests/${{ matrix.project }}" -am \ + # -P run-it \ + # ${{ matrix.pre-start && '-Dinfra-pre-start.enabled=1 ' || '' }} \ + # ${{ matrix.parallel > 0 && format('-Djunit.jupiter.execution.parallel.enabled=true -Djunit.jupiter.execution.parallel.mode.default=concurrent -Djunit.jupiter.execution.parallel.mode.classes.default=concurrent -Djunit.jupiter.execution.parallel.config.strategy=fixed -Djunit.jupiter.execution.parallel.config.fixed.parallelism=2 -Djunit.jupiter.execution.parallel.config.fixed.max-pool-size={0} ', matrix.parallel) || '' }} + + # - name: Create docker image cache + # # Cache can't be saved when it's already present + # if: steps.cache.outputs.cache-hit != 'true' + # run: | + # echo "Images before pruning" && docker image ls + # docker image prune -af --filter "until=2160h" + # docker image prune -f + # echo "Currently available images" && docker image ls + + # echo "Will NOT cache images" && docker image ls -f="label=org.testcontainers.sessionId" + + # mkdir -p ~/.cache/docker + # image_ids_to_save=$(comm -23 <(docker images -q | sort) <(docker image ls -q -f="label=org.testcontainers.sessionId" | sort)) + # echo "Saving image ids" && echo $image_ids_to_save + # docker save $image_ids_to_save -o ~/.cache/docker/images.tar + + # ls -lha ~/.cache/docker + + # - name: Save docker image cache + # if: steps.cache.outputs.cache-hit != 'true' + # uses: actions/cache/save@v6 + # with: + # path: ~/.cache/docker/ + # key: ${{ runner.os }}-it-docker-images-persistence-${{ matrix.project }}-${{ hashFiles('**/pom.xml') }} selenium: strategy: @@ -169,7 +169,6 @@ jobs: run: | echo "Images before pruning" && docker image ls docker image prune -af --filter "until=2160h" - docker image prune -f echo "Currently available images" && docker image ls echo "Will NOT cache images" && docker image ls -f="label=org.testcontainers.sessionId" diff --git a/advanced-demo/pom.xml b/advanced-demo/pom.xml index c2d8d72a..d94323f8 100644 --- a/advanced-demo/pom.xml +++ b/advanced-demo/pom.xml @@ -14,6 +14,8 @@ https://xdev.software + + 21 ${javaVersion} From 47975c138f4e69089b0bd8aa46a47530d98fc133 Mon Sep 17 00:00:00 2001 From: AB Date: Fri, 3 Jul 2026 13:57:10 +0200 Subject: [PATCH 036/141] DEBUG --- .github/workflows/run-integration-tests.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/run-integration-tests.yml b/.github/workflows/run-integration-tests.yml index d6bb3375..5991ebc4 100644 --- a/.github/workflows/run-integration-tests.yml +++ b/.github/workflows/run-integration-tests.yml @@ -167,14 +167,14 @@ jobs: # Cache can't be saved when it's already present if: steps.cache.outputs.cache-hit != 'true' run: | - echo "Images before pruning" && docker image ls + echo "Images before pruning" && docker image ls -a docker image prune -af --filter "until=2160h" - echo "Currently available images" && docker image ls + echo "Currently available images" && docker image ls -a - echo "Will NOT cache images" && docker image ls -f="label=org.testcontainers.sessionId" + echo "Will NOT cache images" && docker image ls -a -f="label=org.testcontainers.sessionId" mkdir -p ~/.cache/docker - image_ids_to_save=$(comm -23 <(docker images -q | sort) <(docker image ls -q -f="label=org.testcontainers.sessionId" | sort)) + image_ids_to_save=$(comm -23 <(docker images -a -q | sort) <(docker image ls -a -q -f="label=org.testcontainers.sessionId" | sort)) echo "Saving image ids" && echo $image_ids_to_save docker save $image_ids_to_save -o ~/.cache/docker/images.tar From 60408c77c77423826c845e22331284103d32e277 Mon Sep 17 00:00:00 2001 From: AB Date: Fri, 3 Jul 2026 13:59:42 +0200 Subject: [PATCH 037/141] DEBUG --- .github/workflows/run-integration-tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run-integration-tests.yml b/.github/workflows/run-integration-tests.yml index 5991ebc4..da8cf417 100644 --- a/.github/workflows/run-integration-tests.yml +++ b/.github/workflows/run-integration-tests.yml @@ -113,8 +113,8 @@ jobs: fail-fast: false matrix: project: [webapp-it] - parallel: [0, 2] - pre-start: [false, true] + parallel: [2] # [0, 2] + pre-start: [true] # [false, true] java: [21] name: "selenium (${{matrix.project}}, ${{matrix.parallel}}, ${{matrix.pre-start}}, ${{matrix.java}})" runs-on: ubuntu-latest From 1dad77099d8efc951e3eba91859abc670be81ed4 Mon Sep 17 00:00:00 2001 From: AB Date: Fri, 3 Jul 2026 14:01:00 +0200 Subject: [PATCH 038/141] DEBUG --- .../java/software/xdev/tci/demo/webapp/cases/LoginOIDCTest.java | 2 ++ .../tci/demo/webapp/cases/LongButLittleResourceUsageTest.java | 2 ++ 2 files changed, 4 insertions(+) diff --git a/advanced-demo/integration-tests/webapp-it/src/test/java/software/xdev/tci/demo/webapp/cases/LoginOIDCTest.java b/advanced-demo/integration-tests/webapp-it/src/test/java/software/xdev/tci/demo/webapp/cases/LoginOIDCTest.java index 73487d3a..a44d709e 100644 --- a/advanced-demo/integration-tests/webapp-it/src/test/java/software/xdev/tci/demo/webapp/cases/LoginOIDCTest.java +++ b/advanced-demo/integration-tests/webapp-it/src/test/java/software/xdev/tci/demo/webapp/cases/LoginOIDCTest.java @@ -1,6 +1,7 @@ package software.xdev.tci.demo.webapp.cases; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; @@ -9,6 +10,7 @@ import software.xdev.tci.selenium.TestBrowser; +@Disabled // TODO class LoginOIDCTest extends InfraPerCaseTest { @DisplayName("Check Login and Logout") diff --git a/advanced-demo/integration-tests/webapp-it/src/test/java/software/xdev/tci/demo/webapp/cases/LongButLittleResourceUsageTest.java b/advanced-demo/integration-tests/webapp-it/src/test/java/software/xdev/tci/demo/webapp/cases/LongButLittleResourceUsageTest.java index 29614c07..6db3e816 100644 --- a/advanced-demo/integration-tests/webapp-it/src/test/java/software/xdev/tci/demo/webapp/cases/LongButLittleResourceUsageTest.java +++ b/advanced-demo/integration-tests/webapp-it/src/test/java/software/xdev/tci/demo/webapp/cases/LongButLittleResourceUsageTest.java @@ -3,6 +3,7 @@ import java.util.stream.IntStream; import java.util.stream.Stream; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -13,6 +14,7 @@ // These tests were written to show the advantages of PreStarting +@Disabled // TODO class LongButLittleResourceUsageTest extends InfraPerCaseTest { static Stream simulatedLongTest() From 748b66ee8291b44c3e496fa7074b2c804ba37edc Mon Sep 17 00:00:00 2001 From: AB Date: Fri, 3 Jul 2026 14:08:49 +0200 Subject: [PATCH 039/141] Update run-integration-tests.yml --- .github/workflows/run-integration-tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/run-integration-tests.yml b/.github/workflows/run-integration-tests.yml index da8cf417..0c2d8a65 100644 --- a/.github/workflows/run-integration-tests.yml +++ b/.github/workflows/run-integration-tests.yml @@ -169,6 +169,7 @@ jobs: run: | echo "Images before pruning" && docker image ls -a docker image prune -af --filter "until=2160h" + # docker image prune -f echo "Currently available images" && docker image ls -a echo "Will NOT cache images" && docker image ls -a -f="label=org.testcontainers.sessionId" From 35ac26d228e24ccb4a6dd3fb1c6ee0f260a2b179 Mon Sep 17 00:00:00 2001 From: AB Date: Fri, 3 Jul 2026 14:23:56 +0200 Subject: [PATCH 040/141] Correctly get image name --- .../xdev/tci/demo/tci/db/containers/DBContainerBuilder.java | 2 +- .../software/xdev/tci/demo/tci/db/factory/DBTCIFactory.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/advanced-demo/integration-tests/tci-db/src/main/java/software/xdev/tci/demo/tci/db/containers/DBContainerBuilder.java b/advanced-demo/integration-tests/tci-db/src/main/java/software/xdev/tci/demo/tci/db/containers/DBContainerBuilder.java index c8e66909..6751fb54 100644 --- a/advanced-demo/integration-tests/tci-db/src/main/java/software/xdev/tci/demo/tci/db/containers/DBContainerBuilder.java +++ b/advanced-demo/integration-tests/tci-db/src/main/java/software/xdev/tci/demo/tci/db/containers/DBContainerBuilder.java @@ -21,7 +21,7 @@ private DBContainerBuilder() { } - public static synchronized String getImageName() + public static String getImageName() { LOG.info("Building Webapp-db-DockerImage..."); diff --git a/advanced-demo/integration-tests/tci-db/src/main/java/software/xdev/tci/demo/tci/db/factory/DBTCIFactory.java b/advanced-demo/integration-tests/tci-db/src/main/java/software/xdev/tci/demo/tci/db/factory/DBTCIFactory.java index 29a9dc15..7b285276 100644 --- a/advanced-demo/integration-tests/tci-db/src/main/java/software/xdev/tci/demo/tci/db/factory/DBTCIFactory.java +++ b/advanced-demo/integration-tests/tci-db/src/main/java/software/xdev/tci/demo/tci/db/factory/DBTCIFactory.java @@ -25,7 +25,7 @@ public DBTCIFactory(final boolean migrateAndInitializeEMC) { super( (c, n) -> new DBTCI(c, n, migrateAndInitializeEMC), - () -> new DBContainer(DBContainerBuilder.getImageName()) + () -> new DBContainer(IMAGE_NAME_SUPPLIER.get()) .withDatabaseName(DBTCI.DB_DATABASE) .withCreateContainerCmdModifier(cmd -> cmd.getHostConfig().withMemory(ContainerMemory.M512M))); this.withSnapshotManager(new CommitedImageSnapshotManager("/var/lib/mysql")); @@ -34,7 +34,7 @@ public DBTCIFactory(final boolean migrateAndInitializeEMC) @Override protected void warmUpInternal() { - DBContainerBuilder.getImageName(); + IMAGE_NAME_SUPPLIER.get(); super.warmUpInternal(); } } From 581e19c6ee1d1af90ab235d8cb414ae4df9b776d Mon Sep 17 00:00:00 2001 From: AB Date: Fri, 3 Jul 2026 14:29:26 +0200 Subject: [PATCH 041/141] DEBUG --- .github/workflows/run-integration-tests.yml | 10 +++++----- .../tci/webapp/containers/WebAppContainerBuilder.java | 7 +++++-- advanced-demo/pom.xml | 2 +- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/.github/workflows/run-integration-tests.yml b/.github/workflows/run-integration-tests.yml index 0c2d8a65..9d23ee9e 100644 --- a/.github/workflows/run-integration-tests.yml +++ b/.github/workflows/run-integration-tests.yml @@ -167,15 +167,15 @@ jobs: # Cache can't be saved when it's already present if: steps.cache.outputs.cache-hit != 'true' run: | - echo "Images before pruning" && docker image ls -a + echo "Images before pruning" && docker image ls docker image prune -af --filter "until=2160h" - # docker image prune -f - echo "Currently available images" && docker image ls -a + docker image prune -f + echo "Currently available images" && docker image ls - echo "Will NOT cache images" && docker image ls -a -f="label=org.testcontainers.sessionId" + echo "Will NOT cache images" && docker image ls -f="label=org.testcontainers.sessionId" mkdir -p ~/.cache/docker - image_ids_to_save=$(comm -23 <(docker images -a -q | sort) <(docker image ls -a -q -f="label=org.testcontainers.sessionId" | sort)) + image_ids_to_save=$(comm -23 <(docker images -q | sort) <(docker image ls -q -f="label=org.testcontainers.sessionId" | sort)) echo "Saving image ids" && echo $image_ids_to_save docker save $image_ids_to_save -o ~/.cache/docker/images.tar diff --git a/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/containers/WebAppContainerBuilder.java b/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/containers/WebAppContainerBuilder.java index bbea26c7..910b6d58 100644 --- a/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/containers/WebAppContainerBuilder.java +++ b/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/containers/WebAppContainerBuilder.java @@ -6,6 +6,7 @@ import java.time.Duration; import java.util.List; import java.util.concurrent.CompletableFuture; +import java.util.stream.Stream; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.slf4j.Logger; @@ -113,8 +114,10 @@ public boolean isIdentical(final List original, final List creat () -> { try { - builder.copyForIntermediateTag("webapp-it-local-builder", "builder") - .build(Duration.ofMinutes(1)); + Stream.of("jre-base", "jre-minimized", "builder") + .forEach(target -> builder.copyForIntermediateTag(target) + .build(Duration.ofMinutes(1))); + builder.cleanCreatedTransferFilesCache(); LOG.info("Tagged intermediate image"); diff --git a/advanced-demo/pom.xml b/advanced-demo/pom.xml index d94323f8..2007e98a 100644 --- a/advanced-demo/pom.xml +++ b/advanced-demo/pom.xml @@ -14,7 +14,7 @@ https://xdev.software - + 21 From e035673ccafc5bc0966852c2f7f37c55105e414c Mon Sep 17 00:00:00 2001 From: AB Date: Fri, 3 Jul 2026 14:30:47 +0200 Subject: [PATCH 042/141] Silence preinstall image prune --- .github/workflows/run-integration-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-integration-tests.yml b/.github/workflows/run-integration-tests.yml index 9d23ee9e..f0172db3 100644 --- a/.github/workflows/run-integration-tests.yml +++ b/.github/workflows/run-integration-tests.yml @@ -150,7 +150,7 @@ jobs: - name: Load docker image cache run: | echo "Deleting preinstalled images" && docker image ls - docker image prune -af + docker image prune -af > /dev/null cat ~/.cache/docker/images.tar | docker load && echo "Restored docker image cache" && docker image ls || true From 1bc8bb1c07654ab9ce76aabae7262014c5dea15a Mon Sep 17 00:00:00 2001 From: AB Date: Wed, 8 Jul 2026 14:31:48 +0200 Subject: [PATCH 043/141] Try to use new caching strategy --- .github/workflows/run-integration-tests.yml | 6 ++- advanced-demo/integration-tests/pom.xml | 2 +- .../containers/WebAppContainerBuilder.java | 45 ++++++------------- .../tci/webapp/factory/WebAppTCIFactory.java | 12 +---- 4 files changed, 20 insertions(+), 45 deletions(-) diff --git a/.github/workflows/run-integration-tests.yml b/.github/workflows/run-integration-tests.yml index f0172db3..a99ae406 100644 --- a/.github/workflows/run-integration-tests.yml +++ b/.github/workflows/run-integration-tests.yml @@ -152,14 +152,16 @@ jobs: echo "Deleting preinstalled images" && docker image ls docker image prune -af > /dev/null - cat ~/.cache/docker/images.tar | docker load && echo "Restored docker image cache" && docker image ls || true + docker load -i ~/.cache/docker/images.tar && echo "Restored docker image cache" && docker image ls || true - name: Test run: | ./mvnw -B test \ -pl "advanced-demo/integration-tests/${{ matrix.project }}" -am \ -P run-it,jacoco \ - -DtagIntermediate=1 \ + -Dapp-docker-image.delete-on-exit=true \ + -Dapp-docker-image.cache-from="type=gha,scope=webapp-gha-1" \ + -Dapp-docker-image.cache-to="type=gha,mode=max,scope=webapp-gha-1" \ ${{ matrix.pre-start && '-Dinfra-pre-start.enabled=1 ' || '' }} \ ${{ matrix.parallel > 0 && format('-Djunit.jupiter.execution.parallel.enabled=true -Djunit.jupiter.execution.parallel.mode.default=concurrent -Djunit.jupiter.execution.parallel.mode.classes.default=concurrent -Djunit.jupiter.execution.parallel.config.strategy=fixed -Djunit.jupiter.execution.parallel.config.fixed.parallelism=2 -Djunit.jupiter.execution.parallel.config.fixed.max-pool-size={0} ', matrix.parallel) || '' }} diff --git a/advanced-demo/integration-tests/pom.xml b/advanced-demo/integration-tests/pom.xml index cea820dc..c38fc0bc 100644 --- a/advanced-demo/integration-tests/pom.xml +++ b/advanced-demo/integration-tests/pom.xml @@ -118,7 +118,7 @@ software.xdev testcontainers-advanced-imagebuilder - 3.0.1 + 4.0.0 diff --git a/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/containers/WebAppContainerBuilder.java b/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/containers/WebAppContainerBuilder.java index 910b6d58..4598c8f3 100644 --- a/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/containers/WebAppContainerBuilder.java +++ b/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/containers/WebAppContainerBuilder.java @@ -1,20 +1,17 @@ package software.xdev.tci.demo.tci.webapp.containers; -import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; import java.time.Duration; import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.stream.Stream; +import java.util.Optional; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import software.xdev.tci.concurrent.TCIExecutorServiceHolder; import software.xdev.tci.jacoco.testbase.config.JaCoCoConfig; -import software.xdev.testcontainers.imagebuilder.AdvancedImageFromDockerFile; +import software.xdev.testcontainers.imagebuilder.buildxnative.NativeAdvancedImageFromDockerfile; import software.xdev.testcontainers.imagebuilder.compat.DockerfileCOPYParentsEmulator; import software.xdev.testcontainers.imagebuilder.transfer.fcm.FileLinesContentModifier; @@ -26,20 +23,21 @@ public final class WebAppContainerBuilder private static final Logger LOG_CONTAINER_BUILD = LoggerFactory.getLogger("container.build.webapp"); + public static final String PROPERTY = "app-docker-image"; + private WebAppContainerBuilder() { } - public static String getImageName(final boolean tagIntermediate) + public static String getImageName() { LOG.info("Building WebApp-DockerImage..."); - final AdvancedImageFromDockerFile builder = - new AdvancedImageFromDockerFile("webapp-it-local", false) + final NativeAdvancedImageFromDockerfile builder = + new NativeAdvancedImageFromDockerfile("webapp-it-local", Boolean.getBoolean(PROPERTY + ".delete-on-exit")) .withLoggerForBuild(LOG_CONTAINER_BUILD) .withDockerFilePath(Paths.get("../../integration-tests/tci-webapp/Dockerfile")) .withBaseDir(Paths.get("../../")) - .withCreateTransferFilesCache(tagIntermediate) .configureFilesToTransferHandler(h -> h .withPostGitIgnoreLines( // Ignore git-folder, as it will be provided in the Dockerfile @@ -83,7 +81,7 @@ public List modify( final List lines, final Path sourcePath, final String targetPath, - final TarArchiveEntry tarArchiveEntry) throws IOException + final TarArchiveEntry tarArchiveEntry) { return lines.stream() // Remove integration tests module @@ -99,6 +97,11 @@ public boolean isIdentical(final List original, final List creat })) ); + Optional.ofNullable(System.getProperty(PROPERTY + ".cache-from")) + .ifPresent(builder::withCacheFrom); + Optional.ofNullable(System.getProperty(PROPERTY + ".cache-to")) + .ifPresent(builder::withCacheTo); + if(JaCoCoConfig.instance().enabled()) { builder.withBuildArg("JACOCO_AGENT_ENABLED", "1"); @@ -108,28 +111,6 @@ public boolean isIdentical(final List original, final List creat LOG.info("Built Image; Name ='{}'", imageName); - if(tagIntermediate) - { - CompletableFuture.runAsync( - () -> { - try - { - Stream.of("jre-base", "jre-minimized", "builder") - .forEach(target -> builder.copyForIntermediateTag(target) - .build(Duration.ofMinutes(1))); - - builder.cleanCreatedTransferFilesCache(); - - LOG.info("Tagged intermediate image"); - } - catch(final Exception ex) - { - LOG.warn("Tagging intermediate target failed", ex); - } - }, - TCIExecutorServiceHolder.instance()); - } - return imageName; } } diff --git a/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/factory/WebAppTCIFactory.java b/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/factory/WebAppTCIFactory.java index 5a36772a..0cc5ef65 100644 --- a/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/factory/WebAppTCIFactory.java +++ b/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/factory/WebAppTCIFactory.java @@ -16,18 +16,10 @@ public class WebAppTCIFactory extends PreStartableTCIFactory { - public static final String PROPERTY_APP_DOCKERIMAGE = "appDockerImage"; - public static final String PROPERTY_TAG_INTERMEDIATE = "tagIntermediate"; - protected static final Supplier IMAGE_NAME_SUPPLIER = Suppliers.memoize(() -> Objects.requireNonNullElseGet( - System.getProperty(PROPERTY_APP_DOCKERIMAGE), - () -> { - String property = System.getProperty(PROPERTY_TAG_INTERMEDIATE); - return WebAppContainerBuilder.getImageName( - "1".equals(property) || Boolean.parseBoolean(property)); - }) - ); + System.getProperty(WebAppContainerBuilder.PROPERTY), + WebAppContainerBuilder::getImageName)); @SuppressWarnings("checkstyle:MagicNumber") public WebAppTCIFactory(final Consumer additionalContainerBuilder) From 7c6ca7c8f7c9ba75fbe14d04633c3edfcfdc3493 Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 9 Jul 2026 09:09:25 +0200 Subject: [PATCH 044/141] Add JavaCoCo config docs --- jacoco/README.md | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/jacoco/README.md b/jacoco/README.md index 61c1b198..def6f75d 100644 --- a/jacoco/README.md +++ b/jacoco/README.md @@ -7,6 +7,33 @@ Provides support for [JaCoCo](https://github.com/jacoco/jacoco) Code Coverage in NOTE: Using this is a bit complex as it also requires some changes to the java application in the container.
Please have a look at the [advanced-demo](../advanced-demo/) and the corresponding [GitHub Actions workflow](../.github/workflows/run-integration-tests.yml) for details. -### Example Report +## Config + +

The configuration is dynamically loaded from (sorted by highest priority) + +* Environment variables + * prefixed with ``TCI_JACOCO_`` + * all properties are in UPPERCASE and use `_` instead of `.` or `-` +* System properties + * prefixed with ``tci.jacoco.`` + +
+ +
Full list of configuration options + +_Please note that the preconfigured values usually work out of the box.
_ +_You should know exactly what you're doing when doing modifications._ + +| Property | Type | Default | Notes | +| --- | --- | --- | --- | +| `enabled` | `bool` | `false` | You should probably set this to `true` | +| `execution-data-files-dir` | `string` | `target/jacoco-execution-data-files` | Location where the jacoco execution reports/execution data files are stored | +| `move-old-execution-data-files-dir` | `bool` | `true` | If `execution-data-files-dir` already exists it will be moved to `execution-data-files-dir-old` | +| `execution-data-files-dir-old` | `string` | `target/jacoco-execution-data-files-old` | Location where previous jacoco execution reports/execution data files are moved to. If the directory already exists it will be deleted. | +| `execution-data-file-suffix` | `string` | `-jacoco.exec` | Suffix for the jacoco reports/execution data files | + +
+ +## Example Report ![](../assets/JaCoCoExample_Overview.avif) ![](../assets/JaCoCoExample_Details.avif) From 596a089876a301482116ecdbe727bc7d6b2695c4 Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 9 Jul 2026 10:10:50 +0200 Subject: [PATCH 045/141] Add build caching --- .github/workflows/run-integration-tests.yml | 21 +- advanced-demo/integration-tests/pom.xml | 12 +- .../integration-tests/tci-db/pom.xml | 9 +- .../tci/db/containers/DBContainerBuilder.java | 27 +- .../integration-tests/tci-webapp/pom.xml | 6 +- .../containers/WebAppContainerBuilder.java | 16 +- .../tci/webapp/factory/WebAppTCIFactory.java | 7 +- bom/pom.xml | 5 + image-build/README.md | 31 ++ image-build/pom.xml | 311 ++++++++++++++++++ .../tci/imagebuild/CreateImageHandler.java | 12 + .../imagebuild/DefaultCreateImageHandler.java | 72 ++++ .../xdev/tci/imagebuild/ImageCreator.java | 28 ++ .../config/CreateImageHandlerConfig.java | 22 ++ .../DefaultCreateImageHandlerConfig.java | 62 ++++ ...are.xdev.tci.imagebuild.CreateImageHandler | 1 + ...imagebuild.config.CreateImageHandlerConfig | 1 + pom.xml | 1 + 18 files changed, 591 insertions(+), 53 deletions(-) create mode 100644 image-build/README.md create mode 100644 image-build/pom.xml create mode 100644 image-build/src/main/java/software/xdev/tci/imagebuild/CreateImageHandler.java create mode 100644 image-build/src/main/java/software/xdev/tci/imagebuild/DefaultCreateImageHandler.java create mode 100644 image-build/src/main/java/software/xdev/tci/imagebuild/ImageCreator.java create mode 100644 image-build/src/main/java/software/xdev/tci/imagebuild/config/CreateImageHandlerConfig.java create mode 100644 image-build/src/main/java/software/xdev/tci/imagebuild/config/DefaultCreateImageHandlerConfig.java create mode 100644 image-build/src/main/resources/META-INF/services/software.xdev.tci.imagebuild.CreateImageHandler create mode 100644 image-build/src/main/resources/META-INF/services/software.xdev.tci.imagebuild.config.CreateImageHandlerConfig diff --git a/.github/workflows/run-integration-tests.yml b/.github/workflows/run-integration-tests.yml index a99ae406..d5270144 100644 --- a/.github/workflows/run-integration-tests.yml +++ b/.github/workflows/run-integration-tests.yml @@ -154,16 +154,31 @@ jobs: docker load -i ~/.cache/docker/images.tar && echo "Restored docker image cache" && docker image ls || true + # These are required for caching + # https://docs.docker.com/build/cache/backends/gha/#authentication + - name: Get Actions Runtime credentials + id: actions-vars + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + core.setOutput('ACTIONS_RUNTIME_TOKEN', process.env['ACTIONS_RUNTIME_TOKEN']) + core.setOutput('ACTIONS_RESULTS_URL', process.env['ACTIONS_RESULTS_URL']) + + - uses: docker/setup-buildx-action@v4 + - name: Test run: | ./mvnw -B test \ -pl "advanced-demo/integration-tests/${{ matrix.project }}" -am \ -P run-it,jacoco \ - -Dapp-docker-image.delete-on-exit=true \ - -Dapp-docker-image.cache-from="type=gha,scope=webapp-gha-1" \ - -Dapp-docker-image.cache-to="type=gha,mode=max,scope=webapp-gha-1" \ + -Dtci.image-build.delete-on-exit=true \ + -Dtci.image-build.cache-from="type=gha,scope=§image" \ + -Dtci.image-build.cache-to="type=gha,mode=max,scope=§image" \ ${{ matrix.pre-start && '-Dinfra-pre-start.enabled=1 ' || '' }} \ ${{ matrix.parallel > 0 && format('-Djunit.jupiter.execution.parallel.enabled=true -Djunit.jupiter.execution.parallel.mode.default=concurrent -Djunit.jupiter.execution.parallel.mode.classes.default=concurrent -Djunit.jupiter.execution.parallel.config.strategy=fixed -Djunit.jupiter.execution.parallel.config.fixed.parallelism=2 -Djunit.jupiter.execution.parallel.config.fixed.max-pool-size={0} ', matrix.parallel) || '' }} + env: + ACTIONS_RUNTIME_TOKEN: ${{ steps.actions-vars.outputs.ACTIONS_RUNTIME_TOKEN }} + ACTIONS_RESULTS_URL: ${{ steps.actions-vars.outputs.ACTIONS_RESULTS_URL }} - name: Create docker image cache # Cache can't be saved when it's already present diff --git a/advanced-demo/integration-tests/pom.xml b/advanced-demo/integration-tests/pom.xml index c38fc0bc..f3278c28 100644 --- a/advanced-demo/integration-tests/pom.xml +++ b/advanced-demo/integration-tests/pom.xml @@ -67,6 +67,11 @@ db-jdbc-spring-orm-hibernate ${project.version}
+ + software.xdev.tci + image-build + ${project.version} + software.xdev.tci jacoco @@ -114,13 +119,6 @@ import - - - software.xdev - testcontainers-advanced-imagebuilder - 4.0.0 - - org.javassist diff --git a/advanced-demo/integration-tests/tci-db/pom.xml b/advanced-demo/integration-tests/tci-db/pom.xml index 8de104dc..a48ccef7 100644 --- a/advanced-demo/integration-tests/tci-db/pom.xml +++ b/advanced-demo/integration-tests/tci-db/pom.xml @@ -21,6 +21,10 @@ software.xdev.tci db-jdbc-spring-orm-hibernate + + software.xdev.tci + image-build + @@ -34,10 +38,5 @@ testcontainers-jdbc compile - - - software.xdev - testcontainers-advanced-imagebuilder -
diff --git a/advanced-demo/integration-tests/tci-db/src/main/java/software/xdev/tci/demo/tci/db/containers/DBContainerBuilder.java b/advanced-demo/integration-tests/tci-db/src/main/java/software/xdev/tci/demo/tci/db/containers/DBContainerBuilder.java index 6751fb54..e05bce43 100644 --- a/advanced-demo/integration-tests/tci-db/src/main/java/software/xdev/tci/demo/tci/db/containers/DBContainerBuilder.java +++ b/advanced-demo/integration-tests/tci-db/src/main/java/software/xdev/tci/demo/tci/db/containers/DBContainerBuilder.java @@ -6,16 +6,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import software.xdev.testcontainers.imagebuilder.AdvancedImageFromDockerFile; +import software.xdev.tci.imagebuild.ImageCreator; +import software.xdev.testcontainers.imagebuilder.buildxnative.NativeAdvancedImageFromDockerfile; -@SuppressWarnings("PMD.MoreThanOneLogger") public final class DBContainerBuilder { private static final Logger LOG = LoggerFactory.getLogger(DBContainerBuilder.class); - private static final Logger LOG_CONTAINER_BUILD = - LoggerFactory.getLogger("container.build.db"); - private DBContainerBuilder() { @@ -25,17 +22,15 @@ public static String getImageName() { LOG.info("Building Webapp-db-DockerImage..."); - final AdvancedImageFromDockerFile builder = - new AdvancedImageFromDockerFile("webapp-db", false) - .withLoggerForBuild(LOG_CONTAINER_BUILD) - .withDockerFilePath(Paths.get("../tci-db/Dockerfile")) - .withBaseDir(Paths.get("../")) - .configureFilesToTransferHandler(h -> h - .withPostGitIgnoreLines( - // Ignore everything - "**") - .withBaseDirRelativeIgnoreFile(null) - ); + final NativeAdvancedImageFromDockerfile builder = ImageCreator.nativeImage("tci-demo-db") + .withDockerFilePath(Paths.get("../tci-db/Dockerfile")) + .withBaseDir(Paths.get("../")) + .configureFilesToTransferHandler(h -> h + .withPostGitIgnoreLines( + // Ignore everything + "**") + .withBaseDirRelativeIgnoreFile(null) + ); final String name = builder.build(Duration.ofMinutes(5)); diff --git a/advanced-demo/integration-tests/tci-webapp/pom.xml b/advanced-demo/integration-tests/tci-webapp/pom.xml index 39fa8d66..e745257f 100644 --- a/advanced-demo/integration-tests/tci-webapp/pom.xml +++ b/advanced-demo/integration-tests/tci-webapp/pom.xml @@ -18,11 +18,11 @@ software.xdev.tci - jacoco + image-build - software.xdev - testcontainers-advanced-imagebuilder + software.xdev.tci + jacoco diff --git a/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/containers/WebAppContainerBuilder.java b/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/containers/WebAppContainerBuilder.java index 4598c8f3..541d5ca8 100644 --- a/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/containers/WebAppContainerBuilder.java +++ b/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/containers/WebAppContainerBuilder.java @@ -4,26 +4,21 @@ import java.nio.file.Paths; import java.time.Duration; import java.util.List; -import java.util.Optional; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import software.xdev.tci.imagebuild.ImageCreator; import software.xdev.tci.jacoco.testbase.config.JaCoCoConfig; import software.xdev.testcontainers.imagebuilder.buildxnative.NativeAdvancedImageFromDockerfile; import software.xdev.testcontainers.imagebuilder.compat.DockerfileCOPYParentsEmulator; import software.xdev.testcontainers.imagebuilder.transfer.fcm.FileLinesContentModifier; -@SuppressWarnings("PMD.MoreThanOneLogger") public final class WebAppContainerBuilder { private static final Logger LOG = LoggerFactory.getLogger(WebAppContainerBuilder.class); - private static final Logger LOG_CONTAINER_BUILD = - LoggerFactory.getLogger("container.build.webapp"); - - public static final String PROPERTY = "app-docker-image"; private WebAppContainerBuilder() { @@ -33,9 +28,7 @@ public static String getImageName() { LOG.info("Building WebApp-DockerImage..."); - final NativeAdvancedImageFromDockerfile builder = - new NativeAdvancedImageFromDockerfile("webapp-it-local", Boolean.getBoolean(PROPERTY + ".delete-on-exit")) - .withLoggerForBuild(LOG_CONTAINER_BUILD) + final NativeAdvancedImageFromDockerfile builder = ImageCreator.nativeImage("tci-demo-webapp") .withDockerFilePath(Paths.get("../../integration-tests/tci-webapp/Dockerfile")) .withBaseDir(Paths.get("../../")) .configureFilesToTransferHandler(h -> h @@ -97,11 +90,6 @@ public boolean isIdentical(final List original, final List creat })) ); - Optional.ofNullable(System.getProperty(PROPERTY + ".cache-from")) - .ifPresent(builder::withCacheFrom); - Optional.ofNullable(System.getProperty(PROPERTY + ".cache-to")) - .ifPresent(builder::withCacheTo); - if(JaCoCoConfig.instance().enabled()) { builder.withBuildArg("JACOCO_AGENT_ENABLED", "1"); diff --git a/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/factory/WebAppTCIFactory.java b/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/factory/WebAppTCIFactory.java index 0cc5ef65..f9ea42fd 100644 --- a/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/factory/WebAppTCIFactory.java +++ b/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/factory/WebAppTCIFactory.java @@ -1,7 +1,6 @@ package software.xdev.tci.demo.tci.webapp.factory; import java.time.Duration; -import java.util.Objects; import java.util.function.Consumer; import java.util.function.Supplier; @@ -16,10 +15,8 @@ public class WebAppTCIFactory extends PreStartableTCIFactory { - protected static final Supplier IMAGE_NAME_SUPPLIER = Suppliers.memoize(() -> - Objects.requireNonNullElseGet( - System.getProperty(WebAppContainerBuilder.PROPERTY), - WebAppContainerBuilder::getImageName)); + protected static final Supplier IMAGE_NAME_SUPPLIER = + Suppliers.memoize(WebAppContainerBuilder::getImageName); @SuppressWarnings("checkstyle:MagicNumber") public WebAppTCIFactory(final Consumer additionalContainerBuilder) diff --git a/bom/pom.xml b/bom/pom.xml index 922ea4fd..fa5a77d4 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -73,6 +73,11 @@ db-jdbc-spring-orm-hibernate 3.5.0-SNAPSHOT + + software.xdev.tci + image-build + 3.5.0-SNAPSHOT + software.xdev.tci jacoco diff --git a/image-build/README.md b/image-build/README.md new file mode 100644 index 00000000..1d81d786 --- /dev/null +++ b/image-build/README.md @@ -0,0 +1,31 @@ +# Image-Build + +Provides common utility and configuration to build images. + +## Config + +
The configuration is dynamically loaded from (sorted by highest priority) + +* Environment variables + * prefixed with ``TCI_IMAGE-BUILD_`` + * all properties are in UPPERCASE and use `_` instead of `.` or `-` +* System properties + * prefixed with ``tci.image-build.`` + +
+ +
Full list of configuration options + +_Please note that the preconfigured values usually work out of the box.
_ +_You should know exactly what you're doing when doing modifications._ + +| Property | Type | Default | Notes | +| --- | --- | --- | --- | +| `delete-on-exit` | `bool` | `false` | Should the image be deleted on exit? | +| `logger-for-build-prefix` | `string` | `container.build.` | Prefix used for the build logger | +| `cache-from` | `string` | - | Only applies to BuildKit (native) build.
See [Docker docs](https://docs.docker.com/build/cache/backends/) for details. | +| `cache-to` | `string` | - | Only applies to BuildKit (native) build.
See [Docker docs](https://docs.docker.com/build/cache/backends/) for details. | + +
+ + diff --git a/image-build/pom.xml b/image-build/pom.xml new file mode 100644 index 00000000..8ab64c14 --- /dev/null +++ b/image-build/pom.xml @@ -0,0 +1,311 @@ + + + 4.0.0 + + software.xdev.tci + image-build + 3.5.0-SNAPSHOT + jar + + image-build + TCI - image-build + https://github.com/xdev-software/tci + + + https://github.com/xdev-software/tci + scm:git:https://github.com/xdev-software/tci.git + + + 2025 + + + XDEV Software + https://xdev.software + + + + + XDEV Software + XDEV Software + https://xdev.software + + + + + + Apache-2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + 21 + ${javaVersion} + + UTF-8 + UTF-8 + + + + + software.xdev.tci + base + 3.5.0-SNAPSHOT + + + software.xdev + testcontainers-advanced-imagebuilder + 4.0.0 + + + + + + + + org.apache.maven.plugins + maven-site-plugin + 4.0.0-M16 + + + org.apache.maven.plugins + maven-project-info-reports-plugin + 3.9.0 + + + + + + com.mycila + license-maven-plugin + 5.0.0 + + + ${project.organization.url} + + + +
com/mycila/maven/plugin/license/templates/APACHE-2.txt
+ + src/main/java/** + src/test/java/** + +
+
+
+ + + first + + format + + process-sources + + +
+ + + org.apache.maven.plugins + maven-compiler-plugin + 3.15.0 + + ${maven.compiler.release} + + -proc:none + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.12.0 + + + attach-javadocs + package + + jar + + + + + true + none + + + + org.apache.maven.plugins + maven-source-plugin + 3.4.0 + + + attach-sources + package + + jar-no-fork + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.6 + +
+
+ + + ignore-service-loading + + + + src/main/resources + + META-INF/services/** + + + + + + + publish + + + + org.codehaus.mojo + flatten-maven-plugin + 1.7.3 + + ossrh + + + + flatten + process-resources + + flatten + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 3.2.8 + + + sign-artifacts + verify + + sign + + + + + + --pinentry-mode + loopback + + + + + + + + + + publish-sonatype-central-portal + + + + org.sonatype.central + central-publishing-maven-plugin + 0.11.0 + true + + sonatype-central-portal + true + + + + + + + checkstyle + + + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.6.0 + + + com.puppycrawl.tools + checkstyle + 13.7.0 + + + + ../.config/checkstyle/checkstyle.xml + true + + + + + check + + + + + + + + + pmd + + + + org.apache.maven.plugins + maven-pmd-plugin + 3.28.0 + + true + true + true + + ../.config/pmd/java/ruleset.xml + + + + + net.sourceforge.pmd + pmd-core + 7.26.0 + + + net.sourceforge.pmd + pmd-java + 7.26.0 + + + + + + + + + + org.apache.maven.plugins + maven-jxr-plugin + 3.6.0 + + + + + +
diff --git a/image-build/src/main/java/software/xdev/tci/imagebuild/CreateImageHandler.java b/image-build/src/main/java/software/xdev/tci/imagebuild/CreateImageHandler.java new file mode 100644 index 00000000..5056a293 --- /dev/null +++ b/image-build/src/main/java/software/xdev/tci/imagebuild/CreateImageHandler.java @@ -0,0 +1,12 @@ +package software.xdev.tci.imagebuild; + +import software.xdev.testcontainers.imagebuilder.AdvancedImageFromDockerFile; +import software.xdev.testcontainers.imagebuilder.buildxnative.NativeAdvancedImageFromDockerfile; + + +public interface CreateImageHandler +{ + NativeAdvancedImageFromDockerfile nativeImage(String dockerImage); + + AdvancedImageFromDockerFile image(String dockerImage); +} diff --git a/image-build/src/main/java/software/xdev/tci/imagebuild/DefaultCreateImageHandler.java b/image-build/src/main/java/software/xdev/tci/imagebuild/DefaultCreateImageHandler.java new file mode 100644 index 00000000..939b6340 --- /dev/null +++ b/image-build/src/main/java/software/xdev/tci/imagebuild/DefaultCreateImageHandler.java @@ -0,0 +1,72 @@ +package software.xdev.tci.imagebuild; + +import java.util.regex.Pattern; + +import org.slf4j.LoggerFactory; + +import software.xdev.tci.imagebuild.config.CreateImageHandlerConfig; +import software.xdev.testcontainers.imagebuilder.AbstractImageFromDockerfile; +import software.xdev.testcontainers.imagebuilder.AdvancedImageFromDockerFile; +import software.xdev.testcontainers.imagebuilder.buildxnative.NativeAdvancedImageFromDockerfile; + + +public class DefaultCreateImageHandler implements CreateImageHandler +{ + protected static final Pattern DOCKER_IMAGE_SANITIZATION_PATTERN = + Pattern.compile("[^A-Za-z0-9-_]"); + + protected final CreateImageHandlerConfig config; + + public DefaultCreateImageHandler() + { + this(CreateImageHandlerConfig.instance()); + } + + public DefaultCreateImageHandler(final CreateImageHandlerConfig config) + { + this.config = config; + } + + @Override + public NativeAdvancedImageFromDockerfile nativeImage(final String dockerImage) + { + final String sanitizeDockerImageName = this.sanitizeDockerImageName(dockerImage); + final NativeAdvancedImageFromDockerfile builder = + new NativeAdvancedImageFromDockerfile(dockerImage, this.config.deleteOnExit()); + + this.configureAbstract(builder, sanitizeDockerImageName); + + this.config.cacheFrom() + .map(s -> s.replace("$image", sanitizeDockerImageName)) + .map(s -> s.replace("§image", sanitizeDockerImageName)) + .ifPresent(builder::withCacheFrom); + this.config.cacheTo() + .map(s -> s.replace("$image", sanitizeDockerImageName)) + .map(s -> s.replace("§image", sanitizeDockerImageName)) + .ifPresent(builder::withCacheTo); + + return builder; + } + + @Override + public AdvancedImageFromDockerFile image(final String dockerImage) + { + final String sanitizeDockerImageName = this.sanitizeDockerImageName(dockerImage); + final AdvancedImageFromDockerFile builder = + new AdvancedImageFromDockerFile(dockerImage, this.config.deleteOnExit()); + + return this.configureAbstract(builder, sanitizeDockerImageName); + } + + protected > I configureAbstract( + final I builder, final String sanitizeDockerImageName) + { + return builder.withLoggerForBuild( + LoggerFactory.getLogger(this.config.loggerForBuildPrefix() + sanitizeDockerImageName)); + } + + protected String sanitizeDockerImageName(final String dockerImage) + { + return DOCKER_IMAGE_SANITIZATION_PATTERN.matcher(dockerImage).replaceAll("_"); + } +} diff --git a/image-build/src/main/java/software/xdev/tci/imagebuild/ImageCreator.java b/image-build/src/main/java/software/xdev/tci/imagebuild/ImageCreator.java new file mode 100644 index 00000000..4be898b5 --- /dev/null +++ b/image-build/src/main/java/software/xdev/tci/imagebuild/ImageCreator.java @@ -0,0 +1,28 @@ +package software.xdev.tci.imagebuild; + +import software.xdev.tci.serviceloading.TCIServiceLoaderHolder; +import software.xdev.testcontainers.imagebuilder.AdvancedImageFromDockerFile; +import software.xdev.testcontainers.imagebuilder.buildxnative.NativeAdvancedImageFromDockerfile; + + +public final class ImageCreator +{ + public static NativeAdvancedImageFromDockerfile nativeImage(final String dockerImage) + { + return impl().nativeImage(dockerImage); + } + + public static AdvancedImageFromDockerFile image(final String dockerImage) + { + return impl().image(dockerImage); + } + + public static CreateImageHandler impl() + { + return TCIServiceLoaderHolder.instance().service(CreateImageHandler.class); + } + + private ImageCreator() + { + } +} diff --git a/image-build/src/main/java/software/xdev/tci/imagebuild/config/CreateImageHandlerConfig.java b/image-build/src/main/java/software/xdev/tci/imagebuild/config/CreateImageHandlerConfig.java new file mode 100644 index 00000000..bfa48e15 --- /dev/null +++ b/image-build/src/main/java/software/xdev/tci/imagebuild/config/CreateImageHandlerConfig.java @@ -0,0 +1,22 @@ +package software.xdev.tci.imagebuild.config; + +import java.util.Optional; + +import software.xdev.tci.serviceloading.TCIServiceLoaderHolder; + + +public interface CreateImageHandlerConfig +{ + boolean deleteOnExit(); + + String loggerForBuildPrefix(); + + Optional cacheFrom(); + + Optional cacheTo(); + + static CreateImageHandlerConfig instance() + { + return TCIServiceLoaderHolder.instance().service(CreateImageHandlerConfig.class); + } +} diff --git a/image-build/src/main/java/software/xdev/tci/imagebuild/config/DefaultCreateImageHandlerConfig.java b/image-build/src/main/java/software/xdev/tci/imagebuild/config/DefaultCreateImageHandlerConfig.java new file mode 100644 index 00000000..d40b1f6e --- /dev/null +++ b/image-build/src/main/java/software/xdev/tci/imagebuild/config/DefaultCreateImageHandlerConfig.java @@ -0,0 +1,62 @@ +package software.xdev.tci.imagebuild.config; + +import java.util.Optional; + +import software.xdev.tci.config.DefaultConfig; + + +@SuppressWarnings({"java:S2789", "OptionalAssignedToNull", "OptionalUsedAsFieldOrParameterType"}) +public class DefaultCreateImageHandlerConfig extends DefaultConfig implements CreateImageHandlerConfig +{ + protected Boolean deleteOnExit; + protected String loggerForBuildPrefix; + protected Optional cacheFrom; + protected Optional cacheTo; + + @Override + protected String propertyNamePrefix() + { + return "tci.image-build"; + } + + @Override + public boolean deleteOnExit() + { + if(this.deleteOnExit == null) + { + this.deleteOnExit = this.resolveBool("delete-on-exit", false); + } + return this.deleteOnExit; + } + + @Override + public String loggerForBuildPrefix() + { + if(this.loggerForBuildPrefix == null) + { + this.loggerForBuildPrefix = this.resolve("logger-for-build-prefix") + .orElse("container.build."); + } + return this.loggerForBuildPrefix; + } + + @Override + public Optional cacheFrom() + { + if(this.cacheFrom == null) + { + this.cacheFrom = this.resolve("cache-from"); + } + return this.cacheFrom; + } + + @Override + public Optional cacheTo() + { + if(this.cacheTo == null) + { + this.cacheTo = this.resolve("cache-to"); + } + return this.cacheTo; + } +} diff --git a/image-build/src/main/resources/META-INF/services/software.xdev.tci.imagebuild.CreateImageHandler b/image-build/src/main/resources/META-INF/services/software.xdev.tci.imagebuild.CreateImageHandler new file mode 100644 index 00000000..f76ede0c --- /dev/null +++ b/image-build/src/main/resources/META-INF/services/software.xdev.tci.imagebuild.CreateImageHandler @@ -0,0 +1 @@ +software.xdev.tci.imagebuild.DefaultCreateImageHandler diff --git a/image-build/src/main/resources/META-INF/services/software.xdev.tci.imagebuild.config.CreateImageHandlerConfig b/image-build/src/main/resources/META-INF/services/software.xdev.tci.imagebuild.config.CreateImageHandlerConfig new file mode 100644 index 00000000..9360b4bb --- /dev/null +++ b/image-build/src/main/resources/META-INF/services/software.xdev.tci.imagebuild.config.CreateImageHandlerConfig @@ -0,0 +1 @@ +software.xdev.tci.imagebuild.config.DefaultCreateImageHandlerConfig diff --git a/pom.xml b/pom.xml index aa90e987..7525bc1f 100644 --- a/pom.xml +++ b/pom.xml @@ -21,6 +21,7 @@ db-jdbc-spring-orm db-jdbc-spring-orm-eclipselink db-jdbc-spring-orm-hibernate + image-build jacoco jul-to-slf4j junit-jupiter-api-support From ebc77887fd7af55995754a4858abcc94561b5d43 Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 9 Jul 2026 10:49:49 +0200 Subject: [PATCH 046/141] Abstract code --- .../docker-image-cache/save/action.yml | 38 ++++++++++++ .../docker-image-cache/setup/action.yml | 56 ++++++++++++++++++ .github/workflows/run-integration-tests.yml | 59 ++++--------------- 3 files changed, 104 insertions(+), 49 deletions(-) create mode 100644 .github/actions/docker-image-cache/save/action.yml create mode 100644 .github/actions/docker-image-cache/setup/action.yml diff --git a/.github/actions/docker-image-cache/save/action.yml b/.github/actions/docker-image-cache/save/action.yml new file mode 100644 index 00000000..b2e168a1 --- /dev/null +++ b/.github/actions/docker-image-cache/save/action.yml @@ -0,0 +1,38 @@ +name: 'Save docker image cache' +description: 'Save the docker image cache' +# Based on https://github.com/actions/cache/blob/v6/restore/action.yml +inputs: + key: + description: 'An explicit key for restoring the cache' + required: true + cache-hit: + description: 'A boolean value to indicate an exact match was found for the primary key' + required: false +runs: + using: "composite" + steps: + - name: Create docker image cache + # Cache can't be saved when it's already present + if: inputs.cache-hit != 'true' + shell: bash + run: | + echo "Images before pruning" && docker image ls + docker image prune -af --filter "until=2160h" + docker image prune -f + echo "Currently available images" && docker image ls + + echo "Will NOT cache images" && docker image ls ${CI_CACHE_DOCKER_SAVE_IGNORE_FILTERS:--f="label=org.testcontainers.sessionId"} + + mkdir -p ~/.cache/docker + image_ids_to_save=$(comm -23 <(docker images -q | sort) <(docker image ls -q ${CI_CACHE_DOCKER_SAVE_IGNORE_FILTERS:--f="label=org.testcontainers.sessionId"} | sort)) + echo "Saving image ids" && echo $image_ids_to_save + docker save $image_ids_to_save -o ~/.cache/docker/images.tar + + ls -lha ~/.cache/docker + + - name: Save docker image cache + if: inputs.cache-hit != 'true' + uses: actions/cache/save@v6 + with: + path: ~/.cache/docker/ + key: ${{ inputs.key }} diff --git a/.github/actions/docker-image-cache/setup/action.yml b/.github/actions/docker-image-cache/setup/action.yml new file mode 100644 index 00000000..0f7c6a86 --- /dev/null +++ b/.github/actions/docker-image-cache/setup/action.yml @@ -0,0 +1,56 @@ +name: 'Restore docker image cache' +description: 'Restore and load docker image cache' +# Based on https://github.com/actions/cache/blob/v6/restore/action.yml +inputs: + key: + description: 'An explicit key for restoring the cache' + required: true + restore-keys: + description: 'An ordered multiline string listing the prefix-matched keys, that are used for restoring stale cache if no cache hit occurred for key. Note `cache-hit` returns false in this case.' + required: false +outputs: + key: + description: 'Identical to the input - can be used to prevent copy pasting' + value: ${{ inputs.key }} + cache-hit: + description: 'A boolean value to indicate an exact match was found for the primary key' + value: ${{ steps.restore-docker-image-cache.outputs.cache-hit }} + ACTIONS_RUNTIME_TOKEN: + value: ${{ steps.actions-vars.outputs.ACTIONS_RUNTIME_TOKEN }} + ACTIONS_RESULTS_URL: + value: ${{ steps.actions-vars.outputs.ACTIONS_RESULTS_URL }} +runs: + using: "composite" + steps: + # RESTORE "NORMAL" DOCKER IMAGE CACHE + - name: Restore docker image cache + uses: actions/cache/restore@v6 + id: restore-docker-image-cache + with: + path: ~/.cache/docker/ + key: ${{ inputs.key }} + restore-keys: ${{ inputs.restore-keys }} + + - name: Load docker image cache + shell: bash + run: | + echo "Deleting preinstalled images" && docker image ls + docker image prune -af > /dev/null + + docker load -i ~/.cache/docker/images.tar && echo "Restored docker image cache" && docker image ls || true + + # CONFIGURE BUILDX FOR CACHING + + # These are required for caching + # https://docs.docker.com/build/cache/backends/gha/#authentication + - name: Get Actions Runtime credentials + if: runner.os != 'Windows' + id: actions-vars + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + core.setOutput('ACTIONS_RUNTIME_TOKEN', process.env['ACTIONS_RUNTIME_TOKEN']) + core.setOutput('ACTIONS_RESULTS_URL', process.env['ACTIONS_RESULTS_URL']) + + - uses: docker/setup-buildx-action@v4 + if: runner.os != 'Windows' diff --git a/.github/workflows/run-integration-tests.yml b/.github/workflows/run-integration-tests.yml index d5270144..ee734ca4 100644 --- a/.github/workflows/run-integration-tests.yml +++ b/.github/workflows/run-integration-tests.yml @@ -137,35 +137,15 @@ jobs: path: ~/.m2/repository key: ${{ runner.os }}-mvn-it-build-selenium-${{ matrix.project }}-${{ hashFiles('**/pom.xml') }} - - name: Restore docker image cache - uses: actions/cache/restore@v6 - id: restore-docker-image-cache + - name: Docker Image Cache Setup + id: docker-image-cache-setup + uses: ./.github/actions/docker-image-cache/setup with: - path: ~/.cache/docker/ - key: ${{ runner.os }}-it-docker-images-selenium-${{ matrix.project }}-${{ hashFiles('**/pom.xml') }} + key: it-docker-images-selenium-${{ matrix.project }}-${{ hashFiles('**/pom.xml') }} restore-keys: | ${{ runner.os }}-it-docker-images-selenium-${{ matrix.project }}-${{ hashFiles('**/pom.xml') }} ${{ runner.os }}-it-docker-images-selenium-${{ matrix.project }}- - - name: Load docker image cache - run: | - echo "Deleting preinstalled images" && docker image ls - docker image prune -af > /dev/null - - docker load -i ~/.cache/docker/images.tar && echo "Restored docker image cache" && docker image ls || true - - # These are required for caching - # https://docs.docker.com/build/cache/backends/gha/#authentication - - name: Get Actions Runtime credentials - id: actions-vars - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - script: | - core.setOutput('ACTIONS_RUNTIME_TOKEN', process.env['ACTIONS_RUNTIME_TOKEN']) - core.setOutput('ACTIONS_RESULTS_URL', process.env['ACTIONS_RESULTS_URL']) - - - uses: docker/setup-buildx-action@v4 - - name: Test run: | ./mvnw -B test \ @@ -177,33 +157,14 @@ jobs: ${{ matrix.pre-start && '-Dinfra-pre-start.enabled=1 ' || '' }} \ ${{ matrix.parallel > 0 && format('-Djunit.jupiter.execution.parallel.enabled=true -Djunit.jupiter.execution.parallel.mode.default=concurrent -Djunit.jupiter.execution.parallel.mode.classes.default=concurrent -Djunit.jupiter.execution.parallel.config.strategy=fixed -Djunit.jupiter.execution.parallel.config.fixed.parallelism=2 -Djunit.jupiter.execution.parallel.config.fixed.max-pool-size={0} ', matrix.parallel) || '' }} env: - ACTIONS_RUNTIME_TOKEN: ${{ steps.actions-vars.outputs.ACTIONS_RUNTIME_TOKEN }} - ACTIONS_RESULTS_URL: ${{ steps.actions-vars.outputs.ACTIONS_RESULTS_URL }} - - - name: Create docker image cache - # Cache can't be saved when it's already present - if: steps.cache.outputs.cache-hit != 'true' - run: | - echo "Images before pruning" && docker image ls - docker image prune -af --filter "until=2160h" - docker image prune -f - echo "Currently available images" && docker image ls - - echo "Will NOT cache images" && docker image ls -f="label=org.testcontainers.sessionId" - - mkdir -p ~/.cache/docker - image_ids_to_save=$(comm -23 <(docker images -q | sort) <(docker image ls -q -f="label=org.testcontainers.sessionId" | sort)) - echo "Saving image ids" && echo $image_ids_to_save - docker save $image_ids_to_save -o ~/.cache/docker/images.tar - - ls -lha ~/.cache/docker + ACTIONS_RUNTIME_TOKEN: ${{ steps.docker-image-cache-setup.outputs.ACTIONS_RUNTIME_TOKEN }} + ACTIONS_RESULTS_URL: ${{ steps.docker-image-cache-setup.outputs.ACTIONS_RESULTS_URL }} - - name: Save docker image cache - if: steps.cache.outputs.cache-hit != 'true' - uses: actions/cache/save@v6 + - name: Docker Image Cache Save + uses: ./.github/actions/docker-image-cache/save with: - path: ~/.cache/docker/ - key: ${{ runner.os }}-it-docker-images-selenium-${{ matrix.project }}-${{ hashFiles('**/pom.xml') }} + key: ${{ steps.docker-image-cache-setup.outputs.key }} + cache-hit: ${{ steps.docker-image-cache-setup.outputs.cache-hit }} - name: Upload videos of test failures if: ${{ failure() }} From 053223736ac4705996debfa393edf3c53f4fa7b1 Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 9 Jul 2026 11:01:08 +0200 Subject: [PATCH 047/141] Test github parallelization --- .../docker-image-cache/setup/action.yml | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/.github/actions/docker-image-cache/setup/action.yml b/.github/actions/docker-image-cache/setup/action.yml index 0f7c6a86..cb07b77e 100644 --- a/.github/actions/docker-image-cache/setup/action.yml +++ b/.github/actions/docker-image-cache/setup/action.yml @@ -23,13 +23,20 @@ runs: using: "composite" steps: # RESTORE "NORMAL" DOCKER IMAGE CACHE - - name: Restore docker image cache - uses: actions/cache/restore@v6 - id: restore-docker-image-cache - with: - path: ~/.cache/docker/ - key: ${{ inputs.key }} - restore-keys: ${{ inputs.restore-keys }} + - parallel: + - name: Delete preinstalled images + shell: bash + run: | + echo "Deleting preinstalled images" && docker image ls + docker image prune -af > /dev/null + + - name: Restore docker image cache + uses: actions/cache/restore@v6 + id: restore-docker-image-cache + with: + path: ~/.cache/docker/ + key: ${{ inputs.key }} + restore-keys: ${{ inputs.restore-keys }} - name: Load docker image cache shell: bash From e2ce726e0f5a7c3cccab45c025e200f49fdedbf7 Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 9 Jul 2026 11:17:34 +0200 Subject: [PATCH 048/141] Use parallel execution --- .../docker-image-cache/setup/action.yml | 28 +++++------ .github/workflows/run-integration-tests.yml | 46 +++++++++++-------- 2 files changed, 41 insertions(+), 33 deletions(-) diff --git a/.github/actions/docker-image-cache/setup/action.yml b/.github/actions/docker-image-cache/setup/action.yml index cb07b77e..19b7ac2b 100644 --- a/.github/actions/docker-image-cache/setup/action.yml +++ b/.github/actions/docker-image-cache/setup/action.yml @@ -23,20 +23,22 @@ runs: using: "composite" steps: # RESTORE "NORMAL" DOCKER IMAGE CACHE - - parallel: - - name: Delete preinstalled images - shell: bash - run: | - echo "Deleting preinstalled images" && docker image ls - docker image prune -af > /dev/null - - name: Restore docker image cache - uses: actions/cache/restore@v6 - id: restore-docker-image-cache - with: - path: ~/.cache/docker/ - key: ${{ inputs.key }} - restore-keys: ${{ inputs.restore-keys }} + # NOTE 2026-07-09 Parallel execution is not yet available for composite actions + # https://github.blog/changelog/2026-06-25-actions-steps-can-now-be-run-in-parallel/ + - name: Delete preinstalled images + shell: bash + run: | + echo "Deleting preinstalled images" && docker image ls + docker image prune -af > /dev/null + + - name: Restore docker image cache + uses: actions/cache/restore@v6 + id: restore-docker-image-cache + with: + path: ~/.cache/docker/ + key: ${{ inputs.key }} + restore-keys: ${{ inputs.restore-keys }} - name: Load docker image cache shell: bash diff --git a/.github/workflows/run-integration-tests.yml b/.github/workflows/run-integration-tests.yml index ee734ca4..92369ca2 100644 --- a/.github/workflows/run-integration-tests.yml +++ b/.github/workflows/run-integration-tests.yml @@ -125,26 +125,27 @@ jobs: steps: - uses: actions/checkout@v7 - - name: Set up JDK - uses: actions/setup-java@v5 - with: - distribution: temurin - java-version: ${{ matrix.java }} - - - name: Cache Maven - uses: actions/cache@v6 - with: - path: ~/.m2/repository - key: ${{ runner.os }}-mvn-it-build-selenium-${{ matrix.project }}-${{ hashFiles('**/pom.xml') }} - - - name: Docker Image Cache Setup - id: docker-image-cache-setup - uses: ./.github/actions/docker-image-cache/setup - with: - key: it-docker-images-selenium-${{ matrix.project }}-${{ hashFiles('**/pom.xml') }} - restore-keys: | - ${{ runner.os }}-it-docker-images-selenium-${{ matrix.project }}-${{ hashFiles('**/pom.xml') }} - ${{ runner.os }}-it-docker-images-selenium-${{ matrix.project }}- + - parallel: + - name: Set up JDK + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: ${{ matrix.java }} + + - name: Cache Maven + uses: actions/cache@v6 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-mvn-it-build-selenium-${{ matrix.project }}-${{ hashFiles('**/pom.xml') }} + + - name: Docker Image Cache Setup + id: docker-image-cache-setup + uses: ./.github/actions/docker-image-cache/setup + with: + key: it-docker-images-selenium-${{ matrix.project }}-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-it-docker-images-selenium-${{ matrix.project }}-${{ hashFiles('**/pom.xml') }} + ${{ runner.os }}-it-docker-images-selenium-${{ matrix.project }}- - name: Test run: | @@ -165,6 +166,7 @@ jobs: with: key: ${{ steps.docker-image-cache-setup.outputs.key }} cache-hit: ${{ steps.docker-image-cache-setup.outputs.cache-hit }} + background: true - name: Upload videos of test failures if: ${{ failure() }} @@ -173,6 +175,7 @@ jobs: name: test-fail-videos-${{ matrix.java }}-${{ matrix.project }}-${{ matrix.parallel }}-${{ matrix.pre-start }} path: advanced-demo/integration-tests/${{ matrix.project }}/target/records if-no-files-found: ignore + background: true - name: Aggregate WebApp JaCoCo report if: ${{ success() }} @@ -187,3 +190,6 @@ jobs: name: webapp-jacoco-report-${{ matrix.java }}-${{ matrix.project }}-${{ matrix.parallel }}-${{ matrix.pre-start }} path: advanced-demo/target/site/jacoco-aggregate if-no-files-found: ignore + + - name: Wait for async jobs to finish + wait-all: From f28fea3d76a69969eff1699ac4859541d2cb6064 Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 9 Jul 2026 12:55:49 +0200 Subject: [PATCH 049/141] Move lock to correct place --- .../tci/serviceloading/TCIServiceLoader.java | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/base/src/main/java/software/xdev/tci/serviceloading/TCIServiceLoader.java b/base/src/main/java/software/xdev/tci/serviceloading/TCIServiceLoader.java index 891a9156..45c8047f 100644 --- a/base/src/main/java/software/xdev/tci/serviceloading/TCIServiceLoader.java +++ b/base/src/main/java/software/xdev/tci/serviceloading/TCIServiceLoader.java @@ -24,6 +24,9 @@ import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + /** * Central point for service loading @@ -31,6 +34,8 @@ @SuppressWarnings({"java:S6548", "java:S2789"}) // Don't force us to write our own Optional!?! public class TCIServiceLoader { + private static final Logger LOG = LoggerFactory.getLogger(TCIServiceLoader.class); + protected final Object globalInitServiceLockHandlerLock = new Object(); protected final InheritableThreadLocal>> tlDetectRecursiveInitServices = new InheritableThreadLocal<>(); @@ -84,9 +89,12 @@ protected void initService(final Class clazz) lock = this.servicesLoadingSyncLocks.computeIfAbsent( clazz, ignored -> new ReentrantLock()); - lock.lock(); + this.reportLockAction("got", clazz); } + lock.lock(); + this.reportLockAction("acquired", clazz); + try { // Already initialized? @@ -97,6 +105,11 @@ protected void initService(final Class clazz) this.loadedServices.put(clazz, this.loadService(clazz)); } + catch(final RuntimeException rex) + { + LOG.debug("Service loading failed", rex); + throw rex; + } finally { synchronized(this.globalInitServiceLockHandlerLock) @@ -106,10 +119,24 @@ protected void initService(final Class clazz) this.servicesLoadingSyncLocks.remove(clazz); lock.unlock(); + this.reportLockAction("release", clazz); } } } + protected void reportLockAction(final String action, final Class clazz) + { + if(LOG.isTraceEnabled()) + { + LOG.trace( + "Lock {} {} - T:{}", + action, + clazz, + Thread.currentThread().getName(), + new RuntimeException()); + } + } + protected Optional loadService(final Class clazz) { return ServiceLoader.load(clazz) From e10067d1d5fcc4757451077499f603534d3cdd7d Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 9 Jul 2026 12:56:35 +0200 Subject: [PATCH 050/141] Execute JULtoSLF4J redirect only once when called multiple times --- .../xdev/tci/logging/JULtoSLF4JRedirector.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/jul-to-slf4j/src/main/java/software/xdev/tci/logging/JULtoSLF4JRedirector.java b/jul-to-slf4j/src/main/java/software/xdev/tci/logging/JULtoSLF4JRedirector.java index be2d0675..6c41107d 100644 --- a/jul-to-slf4j/src/main/java/software/xdev/tci/logging/JULtoSLF4JRedirector.java +++ b/jul-to-slf4j/src/main/java/software/xdev/tci/logging/JULtoSLF4JRedirector.java @@ -35,6 +35,17 @@ protected void redirectInternal() { return; } + + this.redirectInternalSync(); + } + + protected synchronized void redirectInternalSync() + { + if(this.installed) + { + return; + } + if(SLF4JBridgeHandler.isInstalled()) { this.installed = true; From 6b2be2ee6fefcdc26d3c72a97587ea97031b76d6 Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 9 Jul 2026 12:58:17 +0200 Subject: [PATCH 051/141] Build image in background --- .../software/xdev/tci/demo/tci/db/factory/DBTCIFactory.java | 4 +++- .../xdev/tci/demo/tci/webapp/factory/WebAppTCIFactory.java | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/advanced-demo/integration-tests/tci-db/src/main/java/software/xdev/tci/demo/tci/db/factory/DBTCIFactory.java b/advanced-demo/integration-tests/tci-db/src/main/java/software/xdev/tci/demo/tci/db/factory/DBTCIFactory.java index 7b285276..77bd06bd 100644 --- a/advanced-demo/integration-tests/tci-db/src/main/java/software/xdev/tci/demo/tci/db/factory/DBTCIFactory.java +++ b/advanced-demo/integration-tests/tci-db/src/main/java/software/xdev/tci/demo/tci/db/factory/DBTCIFactory.java @@ -1,8 +1,10 @@ package software.xdev.tci.demo.tci.db.factory; +import java.util.concurrent.CompletableFuture; import java.util.function.Supplier; import software.xdev.tci.concurrent.Suppliers; +import software.xdev.tci.concurrent.TCIExecutorServiceHolder; import software.xdev.tci.db.factory.BaseDBTCIFactory; import software.xdev.tci.demo.tci.db.DBTCI; import software.xdev.tci.demo.tci.db.containers.DBContainer; @@ -34,7 +36,7 @@ public DBTCIFactory(final boolean migrateAndInitializeEMC) @Override protected void warmUpInternal() { - IMAGE_NAME_SUPPLIER.get(); + CompletableFuture.runAsync(IMAGE_NAME_SUPPLIER::get, TCIExecutorServiceHolder.instance()); super.warmUpInternal(); } } diff --git a/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/factory/WebAppTCIFactory.java b/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/factory/WebAppTCIFactory.java index f9ea42fd..848b0b7b 100644 --- a/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/factory/WebAppTCIFactory.java +++ b/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/factory/WebAppTCIFactory.java @@ -1,10 +1,12 @@ package software.xdev.tci.demo.tci.webapp.factory; import java.time.Duration; +import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; import java.util.function.Supplier; import software.xdev.tci.concurrent.Suppliers; +import software.xdev.tci.concurrent.TCIExecutorServiceHolder; import software.xdev.tci.demo.tci.webapp.WebAppTCI; import software.xdev.tci.demo.tci.webapp.containers.WebAppContainer; import software.xdev.tci.demo.tci.webapp.containers.WebAppContainerBuilder; @@ -48,7 +50,7 @@ public WebAppTCIFactory(final Consumer additionalContainerBuild @Override protected void warmUpInternal() { - IMAGE_NAME_SUPPLIER.get(); + CompletableFuture.runAsync(IMAGE_NAME_SUPPLIER::get, TCIExecutorServiceHolder.instance()); super.warmUpInternal(); } } From a4577a5867e5c32e21f7d91c08178088afdb08f9 Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 9 Jul 2026 12:58:35 +0200 Subject: [PATCH 052/141] Do not wait forever when warmUp hangs --- .../registry/DefaultTCIFactoryRegistry.java | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/base/src/main/java/software/xdev/tci/factory/registry/DefaultTCIFactoryRegistry.java b/base/src/main/java/software/xdev/tci/factory/registry/DefaultTCIFactoryRegistry.java index c5380c44..95b0658f 100644 --- a/base/src/main/java/software/xdev/tci/factory/registry/DefaultTCIFactoryRegistry.java +++ b/base/src/main/java/software/xdev/tci/factory/registry/DefaultTCIFactoryRegistry.java @@ -21,6 +21,9 @@ import java.util.Set; import java.util.WeakHashMap; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.function.Function; import java.util.stream.Collectors; @@ -58,7 +61,28 @@ public void warmUp() .filter(f -> !this.warmedUpFactories.contains(f)) .map(this::warmUpFactory) .toList() - .forEach(CompletableFuture::join); + .forEach(this::waitForWarmUpCF); + } + } + + protected void waitForWarmUpCF(final CompletableFuture cf) + { + try + { + cf.get(10, TimeUnit.MINUTES); + } + catch(final TimeoutException ex) + { + throw new IllegalStateException("Timed out", ex); + } + catch(final InterruptedException ex) + { + Thread.currentThread().interrupt(); + throw new IllegalStateException("Got interrupted", ex); + } + catch(final ExecutionException ex) + { + throw new IllegalStateException("Execution failed", ex); } } From d2e19bc86dd4ce8457a9af9eb48d9f4136686cd3 Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 9 Jul 2026 12:59:25 +0200 Subject: [PATCH 053/141] Add missing license headers --- .../xdev/tci/imagebuild/CreateImageHandler.java | 15 +++++++++++++++ .../tci/imagebuild/DefaultCreateImageHandler.java | 15 +++++++++++++++ .../xdev/tci/imagebuild/ImageCreator.java | 15 +++++++++++++++ .../config/CreateImageHandlerConfig.java | 15 +++++++++++++++ .../config/DefaultCreateImageHandlerConfig.java | 15 +++++++++++++++ 5 files changed, 75 insertions(+) diff --git a/image-build/src/main/java/software/xdev/tci/imagebuild/CreateImageHandler.java b/image-build/src/main/java/software/xdev/tci/imagebuild/CreateImageHandler.java index 5056a293..b447f6c7 100644 --- a/image-build/src/main/java/software/xdev/tci/imagebuild/CreateImageHandler.java +++ b/image-build/src/main/java/software/xdev/tci/imagebuild/CreateImageHandler.java @@ -1,3 +1,18 @@ +/* + * Copyright © 2025 XDEV Software (https://xdev.software) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package software.xdev.tci.imagebuild; import software.xdev.testcontainers.imagebuilder.AdvancedImageFromDockerFile; diff --git a/image-build/src/main/java/software/xdev/tci/imagebuild/DefaultCreateImageHandler.java b/image-build/src/main/java/software/xdev/tci/imagebuild/DefaultCreateImageHandler.java index 939b6340..efc1814d 100644 --- a/image-build/src/main/java/software/xdev/tci/imagebuild/DefaultCreateImageHandler.java +++ b/image-build/src/main/java/software/xdev/tci/imagebuild/DefaultCreateImageHandler.java @@ -1,3 +1,18 @@ +/* + * Copyright © 2025 XDEV Software (https://xdev.software) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package software.xdev.tci.imagebuild; import java.util.regex.Pattern; diff --git a/image-build/src/main/java/software/xdev/tci/imagebuild/ImageCreator.java b/image-build/src/main/java/software/xdev/tci/imagebuild/ImageCreator.java index 4be898b5..ef452c0d 100644 --- a/image-build/src/main/java/software/xdev/tci/imagebuild/ImageCreator.java +++ b/image-build/src/main/java/software/xdev/tci/imagebuild/ImageCreator.java @@ -1,3 +1,18 @@ +/* + * Copyright © 2025 XDEV Software (https://xdev.software) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package software.xdev.tci.imagebuild; import software.xdev.tci.serviceloading.TCIServiceLoaderHolder; diff --git a/image-build/src/main/java/software/xdev/tci/imagebuild/config/CreateImageHandlerConfig.java b/image-build/src/main/java/software/xdev/tci/imagebuild/config/CreateImageHandlerConfig.java index bfa48e15..80ac8081 100644 --- a/image-build/src/main/java/software/xdev/tci/imagebuild/config/CreateImageHandlerConfig.java +++ b/image-build/src/main/java/software/xdev/tci/imagebuild/config/CreateImageHandlerConfig.java @@ -1,3 +1,18 @@ +/* + * Copyright © 2025 XDEV Software (https://xdev.software) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package software.xdev.tci.imagebuild.config; import java.util.Optional; diff --git a/image-build/src/main/java/software/xdev/tci/imagebuild/config/DefaultCreateImageHandlerConfig.java b/image-build/src/main/java/software/xdev/tci/imagebuild/config/DefaultCreateImageHandlerConfig.java index d40b1f6e..80d09f16 100644 --- a/image-build/src/main/java/software/xdev/tci/imagebuild/config/DefaultCreateImageHandlerConfig.java +++ b/image-build/src/main/java/software/xdev/tci/imagebuild/config/DefaultCreateImageHandlerConfig.java @@ -1,3 +1,18 @@ +/* + * Copyright © 2025 XDEV Software (https://xdev.software) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package software.xdev.tci.imagebuild.config; import java.util.Optional; From a38352f554cbfd6a01553cdd55c2ee4b75369934 Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 9 Jul 2026 13:13:40 +0200 Subject: [PATCH 054/141] Try to improve cache restore speed --- .../actions/docker-image-cache/setup/action.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/actions/docker-image-cache/setup/action.yml b/.github/actions/docker-image-cache/setup/action.yml index 19b7ac2b..7d9a71c4 100644 --- a/.github/actions/docker-image-cache/setup/action.yml +++ b/.github/actions/docker-image-cache/setup/action.yml @@ -26,12 +26,6 @@ runs: # NOTE 2026-07-09 Parallel execution is not yet available for composite actions # https://github.blog/changelog/2026-06-25-actions-steps-can-now-be-run-in-parallel/ - - name: Delete preinstalled images - shell: bash - run: | - echo "Deleting preinstalled images" && docker image ls - docker image prune -af > /dev/null - - name: Restore docker image cache uses: actions/cache/restore@v6 id: restore-docker-image-cache @@ -44,9 +38,15 @@ runs: shell: bash run: | echo "Deleting preinstalled images" && docker image ls - docker image prune -af > /dev/null + image_ids_to_delete=$(docker images -a -q) + echo "Deleting image ids" && echo $image_ids_to_save + # Delete images in background while new images are loaded in + (time docker image rm -f $image_ids_to_delete || true) & + rm_pid=$! + + time docker load -i ~/.cache/docker/images.tar && echo "Restored docker image cache" && docker image ls || true - docker load -i ~/.cache/docker/images.tar && echo "Restored docker image cache" && docker image ls || true + wait $rm_pid # CONFIGURE BUILDX FOR CACHING From e8b25b4204a61a765547c495a571ff79da52e15d Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 9 Jul 2026 13:14:42 +0200 Subject: [PATCH 055/141] Fix var name --- .github/actions/docker-image-cache/setup/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/docker-image-cache/setup/action.yml b/.github/actions/docker-image-cache/setup/action.yml index 7d9a71c4..e607a06a 100644 --- a/.github/actions/docker-image-cache/setup/action.yml +++ b/.github/actions/docker-image-cache/setup/action.yml @@ -39,7 +39,7 @@ runs: run: | echo "Deleting preinstalled images" && docker image ls image_ids_to_delete=$(docker images -a -q) - echo "Deleting image ids" && echo $image_ids_to_save + echo "Deleting image ids" && echo $image_ids_to_delete # Delete images in background while new images are loaded in (time docker image rm -f $image_ids_to_delete || true) & rm_pid=$! From d93fdd5e2a95a6eb1c8b52a94e7b58548b69fc12 Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 9 Jul 2026 13:15:08 +0200 Subject: [PATCH 056/141] Temp disable --- .github/workflows/check-build.yml | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/.github/workflows/check-build.yml b/.github/workflows/check-build.yml index 3db742c4..1b5af9f7 100644 --- a/.github/workflows/check-build.yml +++ b/.github/workflows/check-build.yml @@ -10,14 +10,15 @@ on: - '.github/**' - '.idea/**' - 'assets/**' - pull_request: - branches: [ develop ] - paths-ignore: - - '**.md' - - '.config/**' - - '.github/**' - - '.idea/**' - - 'assets/**' + # TODO + # pull_request: + # branches: [ develop ] + # paths-ignore: + # - '**.md' + # - '.config/**' + # - '.github/**' + # - '.idea/**' + # - 'assets/**' env: DEMO_MAVEN_MODULE: ${{ github.event.repository.name }}-demo From 190fb65a268df29359a84f11aaac121eb0280531 Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 9 Jul 2026 13:20:40 +0200 Subject: [PATCH 057/141] Update action.yml --- .github/actions/docker-image-cache/setup/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/docker-image-cache/setup/action.yml b/.github/actions/docker-image-cache/setup/action.yml index e607a06a..adf91cdd 100644 --- a/.github/actions/docker-image-cache/setup/action.yml +++ b/.github/actions/docker-image-cache/setup/action.yml @@ -37,7 +37,7 @@ runs: - name: Load docker image cache shell: bash run: | - echo "Deleting preinstalled images" && docker image ls + echo "Deleting preinstalled images to not run out of disk space" && docker image ls image_ids_to_delete=$(docker images -a -q) echo "Deleting image ids" && echo $image_ids_to_delete # Delete images in background while new images are loaded in From c55e91c40e8ead87ad8e115e497a7a4c663cdb8e Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 9 Jul 2026 13:21:21 +0200 Subject: [PATCH 058/141] Update run-integration-tests.yml --- .github/workflows/run-integration-tests.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/run-integration-tests.yml b/.github/workflows/run-integration-tests.yml index 92369ca2..61a594ea 100644 --- a/.github/workflows/run-integration-tests.yml +++ b/.github/workflows/run-integration-tests.yml @@ -160,6 +160,10 @@ jobs: env: ACTIONS_RUNTIME_TOKEN: ${{ steps.docker-image-cache-setup.outputs.ACTIONS_RUNTIME_TOKEN }} ACTIONS_RESULTS_URL: ${{ steps.docker-image-cache-setup.outputs.ACTIONS_RESULTS_URL }} + + - name: Display images + run: | + docker image ls - name: Docker Image Cache Save uses: ./.github/actions/docker-image-cache/save From 8ff10fafa869b56892c87f071dedeb1e75610d4b Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 9 Jul 2026 13:22:40 +0200 Subject: [PATCH 059/141] Update action.yml --- .github/actions/docker-image-cache/setup/action.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/actions/docker-image-cache/setup/action.yml b/.github/actions/docker-image-cache/setup/action.yml index adf91cdd..66c97c8d 100644 --- a/.github/actions/docker-image-cache/setup/action.yml +++ b/.github/actions/docker-image-cache/setup/action.yml @@ -44,6 +44,7 @@ runs: (time docker image rm -f $image_ids_to_delete || true) & rm_pid=$! + echo "Importing cache" time docker load -i ~/.cache/docker/images.tar && echo "Restored docker image cache" && docker image ls || true wait $rm_pid From d3dc08438fb6fbcede7f06546a8ad53aa81b78ed Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 9 Jul 2026 13:50:02 +0200 Subject: [PATCH 060/141] Update action.yml --- .github/actions/docker-image-cache/setup/action.yml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/.github/actions/docker-image-cache/setup/action.yml b/.github/actions/docker-image-cache/setup/action.yml index 66c97c8d..e502e799 100644 --- a/.github/actions/docker-image-cache/setup/action.yml +++ b/.github/actions/docker-image-cache/setup/action.yml @@ -37,18 +37,9 @@ runs: - name: Load docker image cache shell: bash run: | - echo "Deleting preinstalled images to not run out of disk space" && docker image ls - image_ids_to_delete=$(docker images -a -q) - echo "Deleting image ids" && echo $image_ids_to_delete - # Delete images in background while new images are loaded in - (time docker image rm -f $image_ids_to_delete || true) & - rm_pid=$! - echo "Importing cache" time docker load -i ~/.cache/docker/images.tar && echo "Restored docker image cache" && docker image ls || true - wait $rm_pid - # CONFIGURE BUILDX FOR CACHING # These are required for caching From cc9d0a97eea0f5d5d97b807f5be72f46ce764a62 Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 9 Jul 2026 14:12:12 +0200 Subject: [PATCH 061/141] Do not cache pre-installed images --- .github/actions/docker-image-cache/save/action.yml | 7 +++++++ .github/actions/docker-image-cache/setup/action.yml | 7 +++++++ advanced-demo/pom.xml | 2 +- 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/actions/docker-image-cache/save/action.yml b/.github/actions/docker-image-cache/save/action.yml index b2e168a1..adab93eb 100644 --- a/.github/actions/docker-image-cache/save/action.yml +++ b/.github/actions/docker-image-cache/save/action.yml @@ -21,10 +21,17 @@ runs: docker image prune -f echo "Currently available images" && docker image ls + preinstalled_image_ids=() + if [ -f ~/.cache/docker-preinstalled/ids.txt ]; then + readarray -t preinstalled_image_ids ~/.cache/docker-preinstalled/ids.txt + echo "Will NOT cache pre-installed images" && echo $preinstalled_image_ids + fi + echo "Will NOT cache images" && docker image ls ${CI_CACHE_DOCKER_SAVE_IGNORE_FILTERS:--f="label=org.testcontainers.sessionId"} mkdir -p ~/.cache/docker image_ids_to_save=$(comm -23 <(docker images -q | sort) <(docker image ls -q ${CI_CACHE_DOCKER_SAVE_IGNORE_FILTERS:--f="label=org.testcontainers.sessionId"} | sort)) + image_ids_to_save=$(comm -23 <(echo $image_ids_to_save) <(echo $preinstalled_image_ids)) echo "Saving image ids" && echo $image_ids_to_save docker save $image_ids_to_save -o ~/.cache/docker/images.tar diff --git a/.github/actions/docker-image-cache/setup/action.yml b/.github/actions/docker-image-cache/setup/action.yml index e502e799..f72b1d78 100644 --- a/.github/actions/docker-image-cache/setup/action.yml +++ b/.github/actions/docker-image-cache/setup/action.yml @@ -37,6 +37,13 @@ runs: - name: Load docker image cache shell: bash run: | + echo "Getting preinstalled images" && docker image ls + preinstalled_image_ids=$(docker images -q) + + echo "Storing preinstalled image ids" && echo $preinstalled_image_ids + mkdir -p ~/.cache/docker-preinstalled + printf "%s\n" "${preinstalled_image_ids[@]}" > ~/.cache/docker-preinstalled/ids.txt + echo "Importing cache" time docker load -i ~/.cache/docker/images.tar && echo "Restored docker image cache" && docker image ls || true diff --git a/advanced-demo/pom.xml b/advanced-demo/pom.xml index 2007e98a..2bce894b 100644 --- a/advanced-demo/pom.xml +++ b/advanced-demo/pom.xml @@ -14,7 +14,7 @@ https://xdev.software - + 21 From 2024acde275ed52757e80f1c75f3ff41de4d16f6 Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 9 Jul 2026 14:43:15 +0200 Subject: [PATCH 062/141] Try to debug problem --- .github/actions/docker-image-cache/save/action.yml | 1 + .github/actions/docker-image-cache/setup/action.yml | 1 + advanced-demo/pom.xml | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/actions/docker-image-cache/save/action.yml b/.github/actions/docker-image-cache/save/action.yml index adab93eb..e697eaa2 100644 --- a/.github/actions/docker-image-cache/save/action.yml +++ b/.github/actions/docker-image-cache/save/action.yml @@ -23,6 +23,7 @@ runs: preinstalled_image_ids=() if [ -f ~/.cache/docker-preinstalled/ids.txt ]; then + cat ~/.cache/docker-preinstalled/ids.txt readarray -t preinstalled_image_ids ~/.cache/docker-preinstalled/ids.txt echo "Will NOT cache pre-installed images" && echo $preinstalled_image_ids fi diff --git a/.github/actions/docker-image-cache/setup/action.yml b/.github/actions/docker-image-cache/setup/action.yml index f72b1d78..e8445f71 100644 --- a/.github/actions/docker-image-cache/setup/action.yml +++ b/.github/actions/docker-image-cache/setup/action.yml @@ -43,6 +43,7 @@ runs: echo "Storing preinstalled image ids" && echo $preinstalled_image_ids mkdir -p ~/.cache/docker-preinstalled printf "%s\n" "${preinstalled_image_ids[@]}" > ~/.cache/docker-preinstalled/ids.txt + cat ~/.cache/docker-preinstalled/ids.txt echo "Importing cache" time docker load -i ~/.cache/docker/images.tar && echo "Restored docker image cache" && docker image ls || true diff --git a/advanced-demo/pom.xml b/advanced-demo/pom.xml index 2bce894b..105b4d43 100644 --- a/advanced-demo/pom.xml +++ b/advanced-demo/pom.xml @@ -14,7 +14,7 @@ https://xdev.software - + 21 From 763e88b35d7ac843565c51ab1a7c65548ca8ac75 Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 9 Jul 2026 15:09:57 +0200 Subject: [PATCH 063/141] Fix readarray --- .github/actions/docker-image-cache/save/action.yml | 3 +-- .github/actions/docker-image-cache/setup/action.yml | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/actions/docker-image-cache/save/action.yml b/.github/actions/docker-image-cache/save/action.yml index e697eaa2..073e3888 100644 --- a/.github/actions/docker-image-cache/save/action.yml +++ b/.github/actions/docker-image-cache/save/action.yml @@ -23,8 +23,7 @@ runs: preinstalled_image_ids=() if [ -f ~/.cache/docker-preinstalled/ids.txt ]; then - cat ~/.cache/docker-preinstalled/ids.txt - readarray -t preinstalled_image_ids ~/.cache/docker-preinstalled/ids.txt + readarray -t preinstalled_image_ids < ~/.cache/docker-preinstalled/ids.txt echo "Will NOT cache pre-installed images" && echo $preinstalled_image_ids fi diff --git a/.github/actions/docker-image-cache/setup/action.yml b/.github/actions/docker-image-cache/setup/action.yml index e8445f71..f72b1d78 100644 --- a/.github/actions/docker-image-cache/setup/action.yml +++ b/.github/actions/docker-image-cache/setup/action.yml @@ -43,7 +43,6 @@ runs: echo "Storing preinstalled image ids" && echo $preinstalled_image_ids mkdir -p ~/.cache/docker-preinstalled printf "%s\n" "${preinstalled_image_ids[@]}" > ~/.cache/docker-preinstalled/ids.txt - cat ~/.cache/docker-preinstalled/ids.txt echo "Importing cache" time docker load -i ~/.cache/docker/images.tar && echo "Restored docker image cache" && docker image ls || true From 688117a979435d43899b1177c4e1bb2ca79a3e73 Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 9 Jul 2026 15:12:06 +0200 Subject: [PATCH 064/141] Update pom.xml --- advanced-demo/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/advanced-demo/pom.xml b/advanced-demo/pom.xml index 105b4d43..696abeae 100644 --- a/advanced-demo/pom.xml +++ b/advanced-demo/pom.xml @@ -14,7 +14,7 @@ https://xdev.software - + 21 From 0b95e1a0002b554b72c871e9374827984450a8f0 Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 9 Jul 2026 15:29:24 +0200 Subject: [PATCH 065/141] TEST --- .github/actions/docker-image-cache/save/action.yml | 6 ++---- .github/actions/docker-image-cache/setup/action.yml | 2 +- advanced-demo/pom.xml | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/actions/docker-image-cache/save/action.yml b/.github/actions/docker-image-cache/save/action.yml index 073e3888..8c132d2d 100644 --- a/.github/actions/docker-image-cache/save/action.yml +++ b/.github/actions/docker-image-cache/save/action.yml @@ -21,17 +21,15 @@ runs: docker image prune -f echo "Currently available images" && docker image ls - preinstalled_image_ids=() if [ -f ~/.cache/docker-preinstalled/ids.txt ]; then - readarray -t preinstalled_image_ids < ~/.cache/docker-preinstalled/ids.txt - echo "Will NOT cache pre-installed images" && echo $preinstalled_image_ids + echo "Will NOT cache pre-installed images" && cat ~/.cache/docker-preinstalled/ids.txt fi echo "Will NOT cache images" && docker image ls ${CI_CACHE_DOCKER_SAVE_IGNORE_FILTERS:--f="label=org.testcontainers.sessionId"} mkdir -p ~/.cache/docker image_ids_to_save=$(comm -23 <(docker images -q | sort) <(docker image ls -q ${CI_CACHE_DOCKER_SAVE_IGNORE_FILTERS:--f="label=org.testcontainers.sessionId"} | sort)) - image_ids_to_save=$(comm -23 <(echo $image_ids_to_save) <(echo $preinstalled_image_ids)) + image_ids_to_save=$(comm -23 <(echo $image_ids_to_save) <(cat ~/.cache/docker-preinstalled/ids.txt || echo "")) echo "Saving image ids" && echo $image_ids_to_save docker save $image_ids_to_save -o ~/.cache/docker/images.tar diff --git a/.github/actions/docker-image-cache/setup/action.yml b/.github/actions/docker-image-cache/setup/action.yml index f72b1d78..d6b50f1e 100644 --- a/.github/actions/docker-image-cache/setup/action.yml +++ b/.github/actions/docker-image-cache/setup/action.yml @@ -38,7 +38,7 @@ runs: shell: bash run: | echo "Getting preinstalled images" && docker image ls - preinstalled_image_ids=$(docker images -q) + preinstalled_image_ids=$(docker images -q | sort) echo "Storing preinstalled image ids" && echo $preinstalled_image_ids mkdir -p ~/.cache/docker-preinstalled diff --git a/advanced-demo/pom.xml b/advanced-demo/pom.xml index 696abeae..080826c2 100644 --- a/advanced-demo/pom.xml +++ b/advanced-demo/pom.xml @@ -14,7 +14,7 @@ https://xdev.software - + 21 From 58c335fad5e36c30cd559454f34752e065b8d777 Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 9 Jul 2026 15:37:33 +0200 Subject: [PATCH 066/141] Update action.yml --- .github/actions/docker-image-cache/save/action.yml | 2 +- advanced-demo/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/docker-image-cache/save/action.yml b/.github/actions/docker-image-cache/save/action.yml index 8c132d2d..ce2f9813 100644 --- a/.github/actions/docker-image-cache/save/action.yml +++ b/.github/actions/docker-image-cache/save/action.yml @@ -29,7 +29,7 @@ runs: mkdir -p ~/.cache/docker image_ids_to_save=$(comm -23 <(docker images -q | sort) <(docker image ls -q ${CI_CACHE_DOCKER_SAVE_IGNORE_FILTERS:--f="label=org.testcontainers.sessionId"} | sort)) - image_ids_to_save=$(comm -23 <(echo $image_ids_to_save) <(cat ~/.cache/docker-preinstalled/ids.txt || echo "")) + image_ids_to_save=$(comm -23 <(echo "${image_ids_to_save[@]}") <(cat ~/.cache/docker-preinstalled/ids.txt || echo "")) echo "Saving image ids" && echo $image_ids_to_save docker save $image_ids_to_save -o ~/.cache/docker/images.tar diff --git a/advanced-demo/pom.xml b/advanced-demo/pom.xml index 080826c2..ba1c94cf 100644 --- a/advanced-demo/pom.xml +++ b/advanced-demo/pom.xml @@ -14,7 +14,7 @@ https://xdev.software - + 21 From 3a6128fe65891c6368d8ea4896b7ff6bac50dd41 Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 9 Jul 2026 15:40:46 +0200 Subject: [PATCH 067/141] Images are loaded from cache without tags so they are always pruned --- .github/actions/docker-image-cache/save/action.yml | 1 - advanced-demo/pom.xml | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/actions/docker-image-cache/save/action.yml b/.github/actions/docker-image-cache/save/action.yml index ce2f9813..1bef6363 100644 --- a/.github/actions/docker-image-cache/save/action.yml +++ b/.github/actions/docker-image-cache/save/action.yml @@ -17,7 +17,6 @@ runs: shell: bash run: | echo "Images before pruning" && docker image ls - docker image prune -af --filter "until=2160h" docker image prune -f echo "Currently available images" && docker image ls diff --git a/advanced-demo/pom.xml b/advanced-demo/pom.xml index ba1c94cf..694c2715 100644 --- a/advanced-demo/pom.xml +++ b/advanced-demo/pom.xml @@ -14,7 +14,7 @@ https://xdev.software - + 21 From 563a51fc9744e52964b80952ec4262dd99f7b23e Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 9 Jul 2026 15:53:41 +0200 Subject: [PATCH 068/141] Add new lines --- .github/actions/docker-image-cache/save/action.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/actions/docker-image-cache/save/action.yml b/.github/actions/docker-image-cache/save/action.yml index 1bef6363..d9e3b341 100644 --- a/.github/actions/docker-image-cache/save/action.yml +++ b/.github/actions/docker-image-cache/save/action.yml @@ -21,15 +21,15 @@ runs: echo "Currently available images" && docker image ls if [ -f ~/.cache/docker-preinstalled/ids.txt ]; then - echo "Will NOT cache pre-installed images" && cat ~/.cache/docker-preinstalled/ids.txt + echo && echo "Will NOT cache pre-installed images" && cat ~/.cache/docker-preinstalled/ids.txt fi - echo "Will NOT cache images" && docker image ls ${CI_CACHE_DOCKER_SAVE_IGNORE_FILTERS:--f="label=org.testcontainers.sessionId"} + echo && echo "Will NOT cache images" && docker image ls ${CI_CACHE_DOCKER_SAVE_IGNORE_FILTERS:--f="label=org.testcontainers.sessionId"} mkdir -p ~/.cache/docker image_ids_to_save=$(comm -23 <(docker images -q | sort) <(docker image ls -q ${CI_CACHE_DOCKER_SAVE_IGNORE_FILTERS:--f="label=org.testcontainers.sessionId"} | sort)) image_ids_to_save=$(comm -23 <(echo "${image_ids_to_save[@]}") <(cat ~/.cache/docker-preinstalled/ids.txt || echo "")) - echo "Saving image ids" && echo $image_ids_to_save + echo && echo "Saving image ids" && echo $image_ids_to_save docker save $image_ids_to_save -o ~/.cache/docker/images.tar ls -lha ~/.cache/docker From 8b1cdf314d61e26ed9a78948ebc53a813d6a6afd Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 9 Jul 2026 16:02:45 +0200 Subject: [PATCH 069/141] Improve speed X --- .github/actions/docker-image-cache/README.md | 5 ++++ .../{setup => restore}/action.yml | 20 ---------------- .../setup-buildx/action.yml | 24 +++++++++++++++++++ .github/workflows/run-integration-tests.yml | 18 ++++++++------ 4 files changed, 40 insertions(+), 27 deletions(-) create mode 100644 .github/actions/docker-image-cache/README.md rename .github/actions/docker-image-cache/{setup => restore}/action.yml (69%) create mode 100644 .github/actions/docker-image-cache/setup-buildx/action.yml diff --git a/.github/actions/docker-image-cache/README.md b/.github/actions/docker-image-cache/README.md new file mode 100644 index 00000000..a54612c9 --- /dev/null +++ b/.github/actions/docker-image-cache/README.md @@ -0,0 +1,5 @@ +# Composite GitHub Actions + +## Notes +* [Parallel execution](https://github.blog/changelog/2026-06-25-actions-steps-can-now-be-run-in-parallel/) is not yet available for composite actions + * Therefore `restore` and `setup-buildx` are currently split as it's recommended that they be implemented in parallel downstream diff --git a/.github/actions/docker-image-cache/setup/action.yml b/.github/actions/docker-image-cache/restore/action.yml similarity index 69% rename from .github/actions/docker-image-cache/setup/action.yml rename to .github/actions/docker-image-cache/restore/action.yml index d6b50f1e..2f160056 100644 --- a/.github/actions/docker-image-cache/setup/action.yml +++ b/.github/actions/docker-image-cache/restore/action.yml @@ -15,10 +15,6 @@ outputs: cache-hit: description: 'A boolean value to indicate an exact match was found for the primary key' value: ${{ steps.restore-docker-image-cache.outputs.cache-hit }} - ACTIONS_RUNTIME_TOKEN: - value: ${{ steps.actions-vars.outputs.ACTIONS_RUNTIME_TOKEN }} - ACTIONS_RESULTS_URL: - value: ${{ steps.actions-vars.outputs.ACTIONS_RESULTS_URL }} runs: using: "composite" steps: @@ -46,19 +42,3 @@ runs: echo "Importing cache" time docker load -i ~/.cache/docker/images.tar && echo "Restored docker image cache" && docker image ls || true - - # CONFIGURE BUILDX FOR CACHING - - # These are required for caching - # https://docs.docker.com/build/cache/backends/gha/#authentication - - name: Get Actions Runtime credentials - if: runner.os != 'Windows' - id: actions-vars - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - script: | - core.setOutput('ACTIONS_RUNTIME_TOKEN', process.env['ACTIONS_RUNTIME_TOKEN']) - core.setOutput('ACTIONS_RESULTS_URL', process.env['ACTIONS_RESULTS_URL']) - - - uses: docker/setup-buildx-action@v4 - if: runner.os != 'Windows' diff --git a/.github/actions/docker-image-cache/setup-buildx/action.yml b/.github/actions/docker-image-cache/setup-buildx/action.yml new file mode 100644 index 00000000..bb986821 --- /dev/null +++ b/.github/actions/docker-image-cache/setup-buildx/action.yml @@ -0,0 +1,24 @@ +name: 'Setup buildx' +description: 'Setup buildx' +# Based on https://github.com/actions/cache/blob/v6/restore/action.yml +outputs: + ACTIONS_RUNTIME_TOKEN: + value: ${{ steps.actions-vars.outputs.ACTIONS_RUNTIME_TOKEN }} + ACTIONS_RESULTS_URL: + value: ${{ steps.actions-vars.outputs.ACTIONS_RESULTS_URL }} +runs: + using: "composite" + steps: + # These are required for caching + # https://docs.docker.com/build/cache/backends/gha/#authentication + - name: Get Actions Runtime credentials + if: runner.os != 'Windows' + id: actions-vars + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + core.setOutput('ACTIONS_RUNTIME_TOKEN', process.env['ACTIONS_RUNTIME_TOKEN']) + core.setOutput('ACTIONS_RESULTS_URL', process.env['ACTIONS_RESULTS_URL']) + + - uses: docker/setup-buildx-action@v4 + if: runner.os != 'Windows' diff --git a/.github/workflows/run-integration-tests.yml b/.github/workflows/run-integration-tests.yml index 61a594ea..db663e6d 100644 --- a/.github/workflows/run-integration-tests.yml +++ b/.github/workflows/run-integration-tests.yml @@ -138,15 +138,19 @@ jobs: path: ~/.m2/repository key: ${{ runner.os }}-mvn-it-build-selenium-${{ matrix.project }}-${{ hashFiles('**/pom.xml') }} - - name: Docker Image Cache Setup - id: docker-image-cache-setup - uses: ./.github/actions/docker-image-cache/setup + - name: Docker Image Cache Restore + id: docker-image-cache-restore + uses: ./.github/actions/docker-image-cache/restore with: key: it-docker-images-selenium-${{ matrix.project }}-${{ hashFiles('**/pom.xml') }} restore-keys: | ${{ runner.os }}-it-docker-images-selenium-${{ matrix.project }}-${{ hashFiles('**/pom.xml') }} ${{ runner.os }}-it-docker-images-selenium-${{ matrix.project }}- + - name: Setup Buildx + id: docker-image-cache-setup-buildx + uses: ./.github/actions/docker-image-cache/setup-buildx + - name: Test run: | ./mvnw -B test \ @@ -158,8 +162,8 @@ jobs: ${{ matrix.pre-start && '-Dinfra-pre-start.enabled=1 ' || '' }} \ ${{ matrix.parallel > 0 && format('-Djunit.jupiter.execution.parallel.enabled=true -Djunit.jupiter.execution.parallel.mode.default=concurrent -Djunit.jupiter.execution.parallel.mode.classes.default=concurrent -Djunit.jupiter.execution.parallel.config.strategy=fixed -Djunit.jupiter.execution.parallel.config.fixed.parallelism=2 -Djunit.jupiter.execution.parallel.config.fixed.max-pool-size={0} ', matrix.parallel) || '' }} env: - ACTIONS_RUNTIME_TOKEN: ${{ steps.docker-image-cache-setup.outputs.ACTIONS_RUNTIME_TOKEN }} - ACTIONS_RESULTS_URL: ${{ steps.docker-image-cache-setup.outputs.ACTIONS_RESULTS_URL }} + ACTIONS_RUNTIME_TOKEN: ${{ steps.docker-image-cache-setup-buildx.outputs.ACTIONS_RUNTIME_TOKEN }} + ACTIONS_RESULTS_URL: ${{ steps.docker-image-cache-setup-buildx.outputs.ACTIONS_RESULTS_URL }} - name: Display images run: | @@ -168,8 +172,8 @@ jobs: - name: Docker Image Cache Save uses: ./.github/actions/docker-image-cache/save with: - key: ${{ steps.docker-image-cache-setup.outputs.key }} - cache-hit: ${{ steps.docker-image-cache-setup.outputs.cache-hit }} + key: ${{ steps.docker-image-cache-restore.outputs.key }} + cache-hit: ${{ steps.docker-image-cache-restore.outputs.cache-hit }} background: true - name: Upload videos of test failures From a0e57ee40bf5f2a1e49246c1d9b8458d766aa6ed Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 9 Jul 2026 16:10:46 +0200 Subject: [PATCH 070/141] Update to Java 25 --- advanced-demo/integration-tests/tci-webapp/Dockerfile | 4 ++-- advanced-demo/webapp/Dockerfile | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/advanced-demo/integration-tests/tci-webapp/Dockerfile b/advanced-demo/integration-tests/tci-webapp/Dockerfile index f48cab54..e8bb371f 100644 --- a/advanced-demo/integration-tests/tci-webapp/Dockerfile +++ b/advanced-demo/integration-tests/tci-webapp/Dockerfile @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:1-labs # Note: This Dockerfile is used by the integration tests for compiling the app when there was no Image was supplied -ARG JAVA_VERSION=21 +ARG JAVA_VERSION=25 FROM eclipse-temurin:${JAVA_VERSION}-jre-alpine AS jre-base @@ -111,7 +111,7 @@ RUN true COPY --from=builder --chown=${user}:${group} /builder/extracted/application/ ./ ENV JAVA_JACOCO_OPTS=${JACOCO_AGENT_ENABLED:+"-javaagent:/jacoco/agent.jar=destfile=/jacoco/report/jacoco.exec"} -ENV JAVA_OPTS="-XX:MaxRAMPercentage=75 -XX:MinHeapFreeRatio=20 -XX:MaxHeapFreeRatio=30 -Djava.awt.headless=true" +ENV JAVA_OPTS="-XX:MaxRAMPercentage=75 -XX:MinHeapFreeRatio=20 -XX:MaxHeapFreeRatio=30 -XX:+UseCompactObjectHeaders -Djava.awt.headless=true" EXPOSE 8080 diff --git a/advanced-demo/webapp/Dockerfile b/advanced-demo/webapp/Dockerfile index 47f962f1..82581fde 100644 --- a/advanced-demo/webapp/Dockerfile +++ b/advanced-demo/webapp/Dockerfile @@ -1,4 +1,4 @@ -FROM eclipse-temurin:21-jre-alpine AS jre-base +FROM eclipse-temurin:25-jre-alpine AS jre-base # Build the JRE ourself and exclude stuff from Eclipse-Temurin that we don't need From e5abdc8500d0d95adb5e694456a81f1f85b313ca Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 9 Jul 2026 16:17:02 +0200 Subject: [PATCH 071/141] Restore commented out code --- .github/workflows/check-build.yml | 17 +-- .github/workflows/run-integration-tests.yml | 157 +++++++++----------- advanced-demo/README.md | 1 + advanced-demo/pom.xml | 2 - 4 files changed, 78 insertions(+), 99 deletions(-) diff --git a/.github/workflows/check-build.yml b/.github/workflows/check-build.yml index 1b5af9f7..3db742c4 100644 --- a/.github/workflows/check-build.yml +++ b/.github/workflows/check-build.yml @@ -10,15 +10,14 @@ on: - '.github/**' - '.idea/**' - 'assets/**' - # TODO - # pull_request: - # branches: [ develop ] - # paths-ignore: - # - '**.md' - # - '.config/**' - # - '.github/**' - # - '.idea/**' - # - 'assets/**' + pull_request: + branches: [ develop ] + paths-ignore: + - '**.md' + - '.config/**' + - '.github/**' + - '.idea/**' + - 'assets/**' env: DEMO_MAVEN_MODULE: ${{ github.event.repository.name }}-demo diff --git a/.github/workflows/run-integration-tests.yml b/.github/workflows/run-integration-tests.yml index db663e6d..7680791e 100644 --- a/.github/workflows/run-integration-tests.yml +++ b/.github/workflows/run-integration-tests.yml @@ -24,97 +24,78 @@ concurrency: cancel-in-progress: true jobs: - # persistence: - # strategy: - # fail-fast: false - # matrix: - # project: [persistence-it-hibernate] - # parallel: [0, 2] - # pre-start: [false, true] - # java: [21] - # include: - # - project: persistence-it-eclipselink - # parallel: 2 - # pre-start: true - # java: 21 - # name: "persistence (${{matrix.project}}, ${{matrix.parallel}}, ${{matrix.pre-start}}, ${{matrix.java}})" - # runs-on: ubuntu-latest - # if: ${{ !(github.event_name == 'pull_request' && startsWith(github.head_ref, 'renovate/')) }} - # timeout-minutes: 10 - # permissions: - # contents: read - # steps: - # - uses: actions/checkout@v7 - - # - name: Set up JDK - # uses: actions/setup-java@v5 - # with: - # distribution: temurin - # java-version: ${{ matrix.java }} - - # - name: Cache Maven - # uses: actions/cache@v6 - # with: - # path: ~/.m2/repository - # key: ${{ runner.os }}-mvn-it-build-persistence-${{ matrix.project }}-${{ hashFiles('**/pom.xml') }} - - # - name: Restore docker image cache - # uses: actions/cache/restore@v6 - # id: restore-docker-image-cache - # with: - # path: ~/.cache/docker/ - # key: ${{ runner.os }}-it-docker-images-persistence-${{ matrix.project }}-${{ hashFiles('**/pom.xml') }} - # restore-keys: | - # ${{ runner.os }}-it-docker-images-persistence-${{ matrix.project }}-${{ hashFiles('**/pom.xml') }} - # ${{ runner.os }}-it-docker-images-persistence-${{ matrix.project }}- - - # - name: Load docker image cache - # run: | - # echo "Deleting preinstalled images" && docker image ls - # docker image prune -af - - # cat ~/.cache/docker/images.tar | docker load && echo "Restored docker image cache" && docker image ls || true - - # - name: Test - # run: | - # ./mvnw -B test \ - # -pl "advanced-demo/integration-tests/${{ matrix.project }}" -am \ - # -P run-it \ - # ${{ matrix.pre-start && '-Dinfra-pre-start.enabled=1 ' || '' }} \ - # ${{ matrix.parallel > 0 && format('-Djunit.jupiter.execution.parallel.enabled=true -Djunit.jupiter.execution.parallel.mode.default=concurrent -Djunit.jupiter.execution.parallel.mode.classes.default=concurrent -Djunit.jupiter.execution.parallel.config.strategy=fixed -Djunit.jupiter.execution.parallel.config.fixed.parallelism=2 -Djunit.jupiter.execution.parallel.config.fixed.max-pool-size={0} ', matrix.parallel) || '' }} - - # - name: Create docker image cache - # # Cache can't be saved when it's already present - # if: steps.cache.outputs.cache-hit != 'true' - # run: | - # echo "Images before pruning" && docker image ls - # docker image prune -af --filter "until=2160h" - # docker image prune -f - # echo "Currently available images" && docker image ls - - # echo "Will NOT cache images" && docker image ls -f="label=org.testcontainers.sessionId" - - # mkdir -p ~/.cache/docker - # image_ids_to_save=$(comm -23 <(docker images -q | sort) <(docker image ls -q -f="label=org.testcontainers.sessionId" | sort)) - # echo "Saving image ids" && echo $image_ids_to_save - # docker save $image_ids_to_save -o ~/.cache/docker/images.tar - - # ls -lha ~/.cache/docker - - # - name: Save docker image cache - # if: steps.cache.outputs.cache-hit != 'true' - # uses: actions/cache/save@v6 - # with: - # path: ~/.cache/docker/ - # key: ${{ runner.os }}-it-docker-images-persistence-${{ matrix.project }}-${{ hashFiles('**/pom.xml') }} + persistence: + strategy: + fail-fast: false + matrix: + project: [persistence-it-hibernate] + parallel: [0, 2] + pre-start: [false, true] + java: [21] + include: + - project: persistence-it-eclipselink + parallel: 2 + pre-start: true + java: 21 + name: "persistence (${{matrix.project}}, ${{matrix.parallel}}, ${{matrix.pre-start}}, ${{matrix.java}})" + runs-on: ubuntu-latest + if: ${{ !(github.event_name == 'pull_request' && startsWith(github.head_ref, 'renovate/')) }} + timeout-minutes: 10 + permissions: + contents: read + steps: + - uses: actions/checkout@v7 + + - parallel: + - name: Set up JDK + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: ${{ matrix.java }} + + - name: Cache Maven + uses: actions/cache@v6 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-mvn-it-build-persistence-${{ matrix.project }}-${{ hashFiles('**/pom.xml') }} + + - name: Docker Image Cache Restore + id: docker-image-cache-restore + uses: ./.github/actions/docker-image-cache/restore + with: + key: it-docker-images-persistence-${{ matrix.project }}-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-it-docker-images-persistence-${{ matrix.project }}-${{ hashFiles('**/pom.xml') }} + ${{ runner.os }}-it-docker-images-persistence-${{ matrix.project }}- + + - name: Setup Buildx + id: docker-image-cache-setup-buildx + uses: ./.github/actions/docker-image-cache/setup-buildx + + - name: Test + run: | + ./mvnw -B test \ + -pl "advanced-demo/integration-tests/${{ matrix.project }}" -am \ + -P run-it \ + ${{ matrix.pre-start && '-Dinfra-pre-start.enabled=1 ' || '' }} \ + ${{ matrix.parallel > 0 && format('-Djunit.jupiter.execution.parallel.enabled=true -Djunit.jupiter.execution.parallel.mode.default=concurrent -Djunit.jupiter.execution.parallel.mode.classes.default=concurrent -Djunit.jupiter.execution.parallel.config.strategy=fixed -Djunit.jupiter.execution.parallel.config.fixed.parallelism=2 -Djunit.jupiter.execution.parallel.config.fixed.max-pool-size={0} ', matrix.parallel) || '' }} + env: + ACTIONS_RUNTIME_TOKEN: ${{ steps.docker-image-cache-setup-buildx.outputs.ACTIONS_RUNTIME_TOKEN }} + ACTIONS_RESULTS_URL: ${{ steps.docker-image-cache-setup-buildx.outputs.ACTIONS_RESULTS_URL }} + + - name: Docker Image Cache Save + uses: ./.github/actions/docker-image-cache/save + with: + key: ${{ steps.docker-image-cache-restore.outputs.key }} + cache-hit: ${{ steps.docker-image-cache-restore.outputs.cache-hit }} selenium: strategy: fail-fast: false matrix: project: [webapp-it] - parallel: [2] # [0, 2] - pre-start: [true] # [false, true] + parallel: [0, 2] + pre-start: [false, true] java: [21] name: "selenium (${{matrix.project}}, ${{matrix.parallel}}, ${{matrix.pre-start}}, ${{matrix.java}})" runs-on: ubuntu-latest @@ -137,7 +118,7 @@ jobs: with: path: ~/.m2/repository key: ${{ runner.os }}-mvn-it-build-selenium-${{ matrix.project }}-${{ hashFiles('**/pom.xml') }} - + - name: Docker Image Cache Restore id: docker-image-cache-restore uses: ./.github/actions/docker-image-cache/restore @@ -164,7 +145,7 @@ jobs: env: ACTIONS_RUNTIME_TOKEN: ${{ steps.docker-image-cache-setup-buildx.outputs.ACTIONS_RUNTIME_TOKEN }} ACTIONS_RESULTS_URL: ${{ steps.docker-image-cache-setup-buildx.outputs.ACTIONS_RESULTS_URL }} - + - name: Display images run: | docker image ls @@ -198,6 +179,6 @@ jobs: name: webapp-jacoco-report-${{ matrix.java }}-${{ matrix.project }}-${{ matrix.parallel }}-${{ matrix.pre-start }} path: advanced-demo/target/site/jacoco-aggregate if-no-files-found: ignore - + - name: Wait for async jobs to finish wait-all: diff --git a/advanced-demo/README.md b/advanced-demo/README.md index b2a0d41e..170dd252 100644 --- a/advanced-demo/README.md +++ b/advanced-demo/README.md @@ -25,3 +25,4 @@ The most interesting project is probably [webapp-it](./integration-tests/webapp- * Safe starting of named containers:
Is integrated inside ``TCI``. Parts of it only kick in on unexpected container start errors. Can also be observed during tests (with e.g. ``docker stats``) as it names the containers. * Container leak detection:
Only kicks in when you forget to stop infrastructure after a test. Can be manually reproduced by commenting out the corresponding ``TCI#stop`` methods. * Tracing:
Seen at the end of each test inside the logs. +* Caching (using GitHub actions):
Cache configuration and usage can be found in [`run-integration-tests.yml`](../.github/workflows/run-integration-tests.yml). diff --git a/advanced-demo/pom.xml b/advanced-demo/pom.xml index 694c2715..c2d8d72a 100644 --- a/advanced-demo/pom.xml +++ b/advanced-demo/pom.xml @@ -14,8 +14,6 @@ https://xdev.software - - 21 ${javaVersion} From 7a4cd416ce751cf9b348e73303206439b6a5153b Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 9 Jul 2026 16:17:45 +0200 Subject: [PATCH 072/141] Remove incorrect comment --- .github/actions/docker-image-cache/setup-buildx/action.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/actions/docker-image-cache/setup-buildx/action.yml b/.github/actions/docker-image-cache/setup-buildx/action.yml index bb986821..ac66f285 100644 --- a/.github/actions/docker-image-cache/setup-buildx/action.yml +++ b/.github/actions/docker-image-cache/setup-buildx/action.yml @@ -1,6 +1,5 @@ name: 'Setup buildx' description: 'Setup buildx' -# Based on https://github.com/actions/cache/blob/v6/restore/action.yml outputs: ACTIONS_RUNTIME_TOKEN: value: ${{ steps.actions-vars.outputs.ACTIONS_RUNTIME_TOKEN }} From ed8b49001001c73274db58d409849dfb7efae15b Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 9 Jul 2026 16:18:39 +0200 Subject: [PATCH 073/141] Update README.md --- .github/actions/docker-image-cache/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/actions/docker-image-cache/README.md b/.github/actions/docker-image-cache/README.md index a54612c9..2e65f24a 100644 --- a/.github/actions/docker-image-cache/README.md +++ b/.github/actions/docker-image-cache/README.md @@ -1,5 +1,7 @@ # Composite GitHub Actions +Example usage can be found in [`run-integration-tests.yml`](../../workflows/run-integration-tests.yml) + ## Notes * [Parallel execution](https://github.blog/changelog/2026-06-25-actions-steps-can-now-be-run-in-parallel/) is not yet available for composite actions * Therefore `restore` and `setup-buildx` are currently split as it's recommended that they be implemented in parallel downstream From a4ca388f3375129b8a7ff27c376f645de5aa5c2f Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 9 Jul 2026 16:20:15 +0200 Subject: [PATCH 074/141] Re-enable tests --- .../java/software/xdev/tci/demo/webapp/cases/LoginOIDCTest.java | 2 -- .../tci/demo/webapp/cases/LongButLittleResourceUsageTest.java | 2 -- 2 files changed, 4 deletions(-) diff --git a/advanced-demo/integration-tests/webapp-it/src/test/java/software/xdev/tci/demo/webapp/cases/LoginOIDCTest.java b/advanced-demo/integration-tests/webapp-it/src/test/java/software/xdev/tci/demo/webapp/cases/LoginOIDCTest.java index a44d709e..73487d3a 100644 --- a/advanced-demo/integration-tests/webapp-it/src/test/java/software/xdev/tci/demo/webapp/cases/LoginOIDCTest.java +++ b/advanced-demo/integration-tests/webapp-it/src/test/java/software/xdev/tci/demo/webapp/cases/LoginOIDCTest.java @@ -1,7 +1,6 @@ package software.xdev.tci.demo.webapp.cases; import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; @@ -10,7 +9,6 @@ import software.xdev.tci.selenium.TestBrowser; -@Disabled // TODO class LoginOIDCTest extends InfraPerCaseTest { @DisplayName("Check Login and Logout") diff --git a/advanced-demo/integration-tests/webapp-it/src/test/java/software/xdev/tci/demo/webapp/cases/LongButLittleResourceUsageTest.java b/advanced-demo/integration-tests/webapp-it/src/test/java/software/xdev/tci/demo/webapp/cases/LongButLittleResourceUsageTest.java index 6db3e816..29614c07 100644 --- a/advanced-demo/integration-tests/webapp-it/src/test/java/software/xdev/tci/demo/webapp/cases/LongButLittleResourceUsageTest.java +++ b/advanced-demo/integration-tests/webapp-it/src/test/java/software/xdev/tci/demo/webapp/cases/LongButLittleResourceUsageTest.java @@ -3,7 +3,6 @@ import java.util.stream.IntStream; import java.util.stream.Stream; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -14,7 +13,6 @@ // These tests were written to show the advantages of PreStarting -@Disabled // TODO class LongButLittleResourceUsageTest extends InfraPerCaseTest { static Stream simulatedLongTest() From 0ba2d117fae62b0ce31f88b37d66bbbef7b51168 Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 9 Jul 2026 16:20:23 +0200 Subject: [PATCH 075/141] Remove no longer needed Dockerfile --- advanced-demo/webapp/.dockerignore | 5 -- advanced-demo/webapp/Dockerfile | 85 ------------------------------ 2 files changed, 90 deletions(-) delete mode 100644 advanced-demo/webapp/.dockerignore delete mode 100644 advanced-demo/webapp/Dockerfile diff --git a/advanced-demo/webapp/.dockerignore b/advanced-demo/webapp/.dockerignore deleted file mode 100644 index c84ae3e6..00000000 --- a/advanced-demo/webapp/.dockerignore +++ /dev/null @@ -1,5 +0,0 @@ -# by default ignore everything! -* - -# allow only things that should be used in the context -!target/webapp.jar diff --git a/advanced-demo/webapp/Dockerfile b/advanced-demo/webapp/Dockerfile deleted file mode 100644 index 82581fde..00000000 --- a/advanced-demo/webapp/Dockerfile +++ /dev/null @@ -1,85 +0,0 @@ -FROM eclipse-temurin:25-jre-alpine AS jre-base - - -# Build the JRE ourself and exclude stuff from Eclipse-Temurin that we don't need -# -# Derived from https://github.com/adoptium/containers/blob/91ea190c462741d2c64ed2f8f0a0efdb3e77c49d/21/jre/alpine/3.21/Dockerfile -FROM alpine:3 AS jre-minimized - -ENV JAVA_HOME=/opt/java/openjdk -ENV PATH=$JAVA_HOME/bin:$PATH - -ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' LC_ALL='en_US.UTF-8' - -RUN set -eux; \ - # DO NOT INSTALL: - # gnupg - only required to verify download of jre from eclipse-temurin - # fontconfig ttf-dejavu - No fonts are needed (we also don't use AWT) - # everything that works with certificates (ca-certificates p11-kit-trust coreutils openssl) - as we don't do stuff with certificates - # tzdata - We are using UTC (and everyone should do that) - apk add --no-cache \ - musl-locales musl-locales-lang - -COPY --from=jre-base /opt/java/openjdk /opt/java/openjdk - -RUN set -eux; \ - echo "Verifying install ..."; \ - echo "java --version"; java --version; \ - echo "Complete." - -# Renamed as cacerts functionality is disabled -COPY --from=jre-base /__cacert_entrypoint.sh /entrypoint.sh -RUN chmod 775 /entrypoint.sh -ENTRYPOINT ["/entrypoint.sh"] - - -# See also https://docs.spring.io/spring-boot/reference/packaging/container-images/dockerfiles.html for further information -FROM jre-minimized AS builder - -WORKDIR /builder - -COPY target/webapp.jar app.jar - -RUN java -Djarmode=tools -jar app.jar extract --layers --destination extracted - - -FROM jre-minimized - -ARG JACOCO_AGENT_ENABLED -ARG JACOCO_AGENT_VERSION="0.8.14" - -ARG user=app -ARG group=app -ARG uid=1000 -ARG gid=1000 -ARG APP_DIR=/opt/app - -# Create user + group + home -RUN mkdir -p ${APP_DIR} \ - && chown ${uid}:${gid} ${APP_DIR} \ - && addgroup -g ${gid} ${group} \ - && adduser -h "$APP_DIR" -u ${uid} -G ${group} -s /bin/bash -D ${user}; \ - # Download JaCoCoAgent if required - if [ "$JACOCO_AGENT_ENABLED" = "1" ]; then \ - mkdir -p /jacoco/report \ - && chown $uid:$gid /jacoco/report \ - && wget https://repo.maven.apache.org/maven2/org/jacoco/org.jacoco.agent/$JACOCO_AGENT_VERSION/org.jacoco.agent-$JACOCO_AGENT_VERSION-runtime.jar -O /jacoco/agent.jar; \ - fi - -WORKDIR ${APP_DIR} - -USER ${user} - -COPY --from=builder --chown=${user}:${group} /builder/extracted/dependencies/ ./ -COPY --from=builder --chown=${user}:${group} /builder/extracted/spring-boot-loader/ ./ -COPY --from=builder --chown=${user}:${group} /builder/extracted/snapshot-dependencies/ ./ -COPY --from=builder --chown=${user}:${group} /builder/extracted/application/ ./ - -# MaxRAMPercentage: Default value is 25% -> we want to use available memory optimal -> increased, but enough is left for other RAM usages like e.g. Metaspace -# Min/MaxHeapFreeRatio: Default values cause container reserved memory not to shrink properly/waste memory -> decreased -# https://stackoverflow.com/questions/16058250/what-is-the-purpose-of-xxminheapfreeratio-and-xxmaxheapfreeratio -ENV JAVA_OPTS="-XX:MaxRAMPercentage=75 -XX:MinHeapFreeRatio=20 -XX:MaxHeapFreeRatio=30 -Djava.awt.headless=true" - -EXPOSE 8080 - -CMD [ "/bin/sh", "-c", "java $JAVA_OPTS -jar app.jar" ] From 934e934126ccca5ae837f4c31185663b048e380f Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 9 Jul 2026 16:29:42 +0200 Subject: [PATCH 076/141] Update docs --- .../actions/docker-image-cache/restore/README.md | 1 + .github/actions/docker-image-cache/save/README.md | 1 + .../docker-image-cache/setup-buildx/README.md | 1 + CHANGELOG.md | 13 +++++++++++++ README.md | 3 +++ 5 files changed, 19 insertions(+) create mode 100644 .github/actions/docker-image-cache/restore/README.md create mode 100644 .github/actions/docker-image-cache/save/README.md create mode 100644 .github/actions/docker-image-cache/setup-buildx/README.md diff --git a/.github/actions/docker-image-cache/restore/README.md b/.github/actions/docker-image-cache/restore/README.md new file mode 100644 index 00000000..5f6ad4c8 --- /dev/null +++ b/.github/actions/docker-image-cache/restore/README.md @@ -0,0 +1 @@ +Restores previously downloaded images so that they don't need to be pulled diff --git a/.github/actions/docker-image-cache/save/README.md b/.github/actions/docker-image-cache/save/README.md new file mode 100644 index 00000000..6eef1df3 --- /dev/null +++ b/.github/actions/docker-image-cache/save/README.md @@ -0,0 +1 @@ +Saves the pulled images into the cache diff --git a/.github/actions/docker-image-cache/setup-buildx/README.md b/.github/actions/docker-image-cache/setup-buildx/README.md new file mode 100644 index 00000000..09e54fd5 --- /dev/null +++ b/.github/actions/docker-image-cache/setup-buildx/README.md @@ -0,0 +1 @@ +Set's up the buildx worker and returns [required environment variables](https://docs.docker.com/build/cache/backends/gha/#authentication) for the buildx build. diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c407230..09736482 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +# 4.0.0 +* Added `image-build` module + * This module utilizes v4 of [testcontainers-advanced-imagebuilder](https://github.com/xdev-software/testcontainers-advanced-imagebuilder) + * It contains some shortcuts that help with common configuration e.g. related to caching (see below) +* Added dedicated composite actions for caching + * These can be found in `.github/actions/docker-image-cache` + * Currently 3 actions exist: + * `restore` - Restores the previously downloaded images + * `setup-buildx` - Set's up the buildx worker and returns required environment variables + * `save` - Saves the downloaded images +* Updated demo +* Fixed a deadlock during service resolution + # 3.5.0 * `db-jdbc-spring-orm` & implementations * No longer use `setPersistenceProviderClassName` because it has no effect diff --git a/README.md b/README.md index bef64902..761ce302 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,9 @@ Modules for XDEV's Testcontainer Infrastructure Framework * Data-generation template * Improved JDBC Container wait strategy * Predefined implementations for [Spring-ORM](./db-jdbc-spring-orm/), [Hibernate](./db-jdbc-spring-orm-hibernate/) and [EclipseLink](./db-jdbc-spring-orm-eclipselink/) +* [image-build](./image-build/) + * Contains some shortcuts that help with common build configuration e.g. related to caching + * Designed to work together with the [`docker-image-cache` actions](./.github/actions/docker-image-cache/) * [jacoco](./jacoco/) * Allows for recording of JaCoCo code coverage files with Java containers * [jul-to-slf4j](./jul-to-slf4j/) From 296683ce98911b4e0823990c6bfd5009554bc484 Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 9 Jul 2026 16:31:23 +0200 Subject: [PATCH 077/141] Bump and consolidate versions --- CHANGELOG.md | 6 ++-- advanced-demo/entities-metamodel/pom.xml | 2 +- advanced-demo/entities/pom.xml | 2 +- .../persistence-it-eclipselink/pom.xml | 2 +- .../persistence-it-hibernate/pom.xml | 2 +- .../integration-tests/persistence-it/pom.xml | 2 +- advanced-demo/integration-tests/pom.xml | 10 +++---- .../integration-tests/tci-db/pom.xml | 2 +- .../integration-tests/tci-webapp/pom.xml | 2 +- .../integration-tests/webapp-it/pom.xml | 2 +- advanced-demo/persistence/pom.xml | 2 +- advanced-demo/pom.xml | 8 +++--- advanced-demo/webapp/pom.xml | 2 +- base-demo/pom.xml | 4 +-- base/pom.xml | 2 +- bom/pom.xml | 28 +++++++++---------- db-jdbc-spring-orm-eclipselink/pom.xml | 4 +-- db-jdbc-spring-orm-hibernate/pom.xml | 4 +-- db-jdbc-spring-orm/pom.xml | 4 +-- db-jdbc/pom.xml | 4 +-- image-build/pom.xml | 4 +-- jacoco/pom.xml | 4 +-- jul-to-slf4j/pom.xml | 2 +- junit-jupiter-api-support/pom.xml | 2 +- mockserver/pom.xml | 4 +-- oidc-server-mock/pom.xml | 4 +-- pom.xml | 2 +- selenium/pom.xml | 8 +++--- spring-dao-support/pom.xml | 4 +-- 29 files changed, 63 insertions(+), 65 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 09736482..bd93637a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,14 +8,12 @@ * `restore` - Restores the previously downloaded images * `setup-buildx` - Set's up the buildx worker and returns required environment variables * `save` - Saves the downloaded images -* Updated demo -* Fixed a deadlock during service resolution - -# 3.5.0 * `db-jdbc-spring-orm` & implementations * No longer use `setPersistenceProviderClassName` because it has no effect * Performance: Reuse `PersistenceProvider` +* Fixed a deadlock during service resolution * Updated dependencies +* Updated demo # 3.4.1 * Remove dedicated code for Java 17 (no longer needed because Java 21 is the minimum) diff --git a/advanced-demo/entities-metamodel/pom.xml b/advanced-demo/entities-metamodel/pom.xml index 73d24334..d9ffd8cc 100644 --- a/advanced-demo/entities-metamodel/pom.xml +++ b/advanced-demo/entities-metamodel/pom.xml @@ -7,7 +7,7 @@ software.xdev.tci.demo advanced-demo - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT entities-metamodel diff --git a/advanced-demo/entities/pom.xml b/advanced-demo/entities/pom.xml index c5975091..4dd287e0 100644 --- a/advanced-demo/entities/pom.xml +++ b/advanced-demo/entities/pom.xml @@ -7,7 +7,7 @@ software.xdev.tci.demo advanced-demo - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT entities diff --git a/advanced-demo/integration-tests/persistence-it-eclipselink/pom.xml b/advanced-demo/integration-tests/persistence-it-eclipselink/pom.xml index 0062e2db..74533ccd 100644 --- a/advanced-demo/integration-tests/persistence-it-eclipselink/pom.xml +++ b/advanced-demo/integration-tests/persistence-it-eclipselink/pom.xml @@ -7,7 +7,7 @@ software.xdev.tci.demo.it integration-tests - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT persistence-it-eclipselink diff --git a/advanced-demo/integration-tests/persistence-it-hibernate/pom.xml b/advanced-demo/integration-tests/persistence-it-hibernate/pom.xml index 8728d38d..cd86b3ca 100644 --- a/advanced-demo/integration-tests/persistence-it-hibernate/pom.xml +++ b/advanced-demo/integration-tests/persistence-it-hibernate/pom.xml @@ -7,7 +7,7 @@ software.xdev.tci.demo.it integration-tests - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT persistence-it-hibernate diff --git a/advanced-demo/integration-tests/persistence-it/pom.xml b/advanced-demo/integration-tests/persistence-it/pom.xml index b3b2f7d8..73fd57cd 100644 --- a/advanced-demo/integration-tests/persistence-it/pom.xml +++ b/advanced-demo/integration-tests/persistence-it/pom.xml @@ -7,7 +7,7 @@ software.xdev.tci.demo.it integration-tests - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT persistence-it diff --git a/advanced-demo/integration-tests/pom.xml b/advanced-demo/integration-tests/pom.xml index f3278c28..07788024 100644 --- a/advanced-demo/integration-tests/pom.xml +++ b/advanced-demo/integration-tests/pom.xml @@ -7,12 +7,12 @@ software.xdev.tci.demo advanced-demo - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT software.xdev.tci.demo.it integration-tests - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT pom @@ -30,7 +30,7 @@ software.xdev.tci.demo.it persistence-it - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT test-jar tests @@ -38,13 +38,13 @@ software.xdev.tci.demo.it tci-db - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT software.xdev.tci.demo.it tci-webapp - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT diff --git a/advanced-demo/integration-tests/tci-db/pom.xml b/advanced-demo/integration-tests/tci-db/pom.xml index a48ccef7..321c5647 100644 --- a/advanced-demo/integration-tests/tci-db/pom.xml +++ b/advanced-demo/integration-tests/tci-db/pom.xml @@ -7,7 +7,7 @@ software.xdev.tci.demo.it integration-tests - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT tci-db diff --git a/advanced-demo/integration-tests/tci-webapp/pom.xml b/advanced-demo/integration-tests/tci-webapp/pom.xml index e745257f..2ddfffcd 100644 --- a/advanced-demo/integration-tests/tci-webapp/pom.xml +++ b/advanced-demo/integration-tests/tci-webapp/pom.xml @@ -7,7 +7,7 @@ software.xdev.tci.demo.it integration-tests - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT tci-webapp diff --git a/advanced-demo/integration-tests/webapp-it/pom.xml b/advanced-demo/integration-tests/webapp-it/pom.xml index 1cfe501e..1e713731 100644 --- a/advanced-demo/integration-tests/webapp-it/pom.xml +++ b/advanced-demo/integration-tests/webapp-it/pom.xml @@ -7,7 +7,7 @@ software.xdev.tci.demo.it integration-tests - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT webapp-it diff --git a/advanced-demo/persistence/pom.xml b/advanced-demo/persistence/pom.xml index e9fb9f28..363a9c36 100644 --- a/advanced-demo/persistence/pom.xml +++ b/advanced-demo/persistence/pom.xml @@ -7,7 +7,7 @@ software.xdev.tci.demo advanced-demo - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT persistence diff --git a/advanced-demo/pom.xml b/advanced-demo/pom.xml index c2d8d72a..7b2e3021 100644 --- a/advanced-demo/pom.xml +++ b/advanced-demo/pom.xml @@ -6,7 +6,7 @@ software.xdev.tci.demo advanced-demo - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT pom @@ -38,19 +38,19 @@ software.xdev.tci.demo entities - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT software.xdev.tci.demo entities-metamodel - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT software.xdev.tci.demo persistence - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT diff --git a/advanced-demo/webapp/pom.xml b/advanced-demo/webapp/pom.xml index 4a0f2d57..8c853a8c 100644 --- a/advanced-demo/webapp/pom.xml +++ b/advanced-demo/webapp/pom.xml @@ -7,7 +7,7 @@ software.xdev.tci.demo advanced-demo - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT webapp diff --git a/base-demo/pom.xml b/base-demo/pom.xml index d755ccd3..4cad8df5 100644 --- a/base-demo/pom.xml +++ b/base-demo/pom.xml @@ -7,11 +7,11 @@ software.xdev.tci root - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT base-demo - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT jar diff --git a/base/pom.xml b/base/pom.xml index 51b2e686..42c60a2d 100644 --- a/base/pom.xml +++ b/base/pom.xml @@ -6,7 +6,7 @@ software.xdev.tci base - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT jar base diff --git a/bom/pom.xml b/bom/pom.xml index fa5a77d4..7f6e1f83 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -6,7 +6,7 @@ software.xdev.tci bom - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT pom bom @@ -51,67 +51,67 @@ software.xdev.tci base - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT software.xdev.tci db-jdbc - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT software.xdev.tci db-jdbc-spring-orm - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT software.xdev.tci db-jdbc-spring-orm-eclipselink - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT software.xdev.tci db-jdbc-spring-orm-hibernate - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT software.xdev.tci image-build - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT software.xdev.tci jacoco - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT software.xdev.tci jul-to-slf4j - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT software.xdev.tci junit-jupiter-api-support - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT software.xdev.tci mockserver - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT software.xdev.tci oidc-server-mock - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT software.xdev.tci selenium - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT software.xdev.tci spring-dao-support - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT diff --git a/db-jdbc-spring-orm-eclipselink/pom.xml b/db-jdbc-spring-orm-eclipselink/pom.xml index 61a4205c..a7689a23 100644 --- a/db-jdbc-spring-orm-eclipselink/pom.xml +++ b/db-jdbc-spring-orm-eclipselink/pom.xml @@ -6,7 +6,7 @@ software.xdev.tci db-jdbc-spring-orm-eclipselink - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT jar db-jdbc-spring-orm-eclipselink @@ -53,7 +53,7 @@ software.xdev.tci db-jdbc-spring-orm - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT diff --git a/db-jdbc-spring-orm-hibernate/pom.xml b/db-jdbc-spring-orm-hibernate/pom.xml index 050ae61c..fdc1bca8 100644 --- a/db-jdbc-spring-orm-hibernate/pom.xml +++ b/db-jdbc-spring-orm-hibernate/pom.xml @@ -6,7 +6,7 @@ software.xdev.tci db-jdbc-spring-orm-hibernate - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT jar db-jdbc-spring-orm-hibernate @@ -53,7 +53,7 @@ software.xdev.tci db-jdbc-spring-orm - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT diff --git a/db-jdbc-spring-orm/pom.xml b/db-jdbc-spring-orm/pom.xml index 2cac28e2..c19b8c88 100644 --- a/db-jdbc-spring-orm/pom.xml +++ b/db-jdbc-spring-orm/pom.xml @@ -6,7 +6,7 @@ software.xdev.tci db-jdbc-spring-orm - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT jar db-jdbc-spring-orm @@ -53,7 +53,7 @@ software.xdev.tci db-jdbc - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT diff --git a/db-jdbc/pom.xml b/db-jdbc/pom.xml index e8bcad86..c1fbbc64 100644 --- a/db-jdbc/pom.xml +++ b/db-jdbc/pom.xml @@ -6,7 +6,7 @@ software.xdev.tci db-jdbc - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT jar db-jdbc @@ -53,7 +53,7 @@ software.xdev.tci base - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT diff --git a/image-build/pom.xml b/image-build/pom.xml index 8ab64c14..093e6939 100644 --- a/image-build/pom.xml +++ b/image-build/pom.xml @@ -6,7 +6,7 @@ software.xdev.tci image-build - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT jar image-build @@ -53,7 +53,7 @@ software.xdev.tci base - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT software.xdev diff --git a/jacoco/pom.xml b/jacoco/pom.xml index 3aa36b09..14a24c07 100644 --- a/jacoco/pom.xml +++ b/jacoco/pom.xml @@ -6,7 +6,7 @@ software.xdev.tci jacoco - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT jar jacoco @@ -53,7 +53,7 @@ software.xdev.tci base - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT diff --git a/jul-to-slf4j/pom.xml b/jul-to-slf4j/pom.xml index 6e29cfe9..c7018e70 100644 --- a/jul-to-slf4j/pom.xml +++ b/jul-to-slf4j/pom.xml @@ -6,7 +6,7 @@ software.xdev.tci jul-to-slf4j - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT jar jul-to-slf4j diff --git a/junit-jupiter-api-support/pom.xml b/junit-jupiter-api-support/pom.xml index 3d7058bf..12fb28f2 100644 --- a/junit-jupiter-api-support/pom.xml +++ b/junit-jupiter-api-support/pom.xml @@ -6,7 +6,7 @@ software.xdev.tci junit-jupiter-api-support - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT jar junit-jupiter-api-support diff --git a/mockserver/pom.xml b/mockserver/pom.xml index ebf8fc6e..f5740aa8 100644 --- a/mockserver/pom.xml +++ b/mockserver/pom.xml @@ -6,7 +6,7 @@ software.xdev.tci mockserver - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT jar mockserver @@ -65,7 +65,7 @@ software.xdev.tci base - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT diff --git a/oidc-server-mock/pom.xml b/oidc-server-mock/pom.xml index a1727262..b3dd16a4 100644 --- a/oidc-server-mock/pom.xml +++ b/oidc-server-mock/pom.xml @@ -6,7 +6,7 @@ software.xdev.tci oidc-server-mock - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT jar oidc-server-mock @@ -53,7 +53,7 @@ software.xdev.tci base - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT diff --git a/pom.xml b/pom.xml index 7525bc1f..496b07d5 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ software.xdev.tci root - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT pom diff --git a/selenium/pom.xml b/selenium/pom.xml index 98e1c7c5..e94800ab 100644 --- a/selenium/pom.xml +++ b/selenium/pom.xml @@ -6,7 +6,7 @@ software.xdev.tci selenium - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT jar selenium @@ -65,18 +65,18 @@ software.xdev.tci base - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT software.xdev.tci jul-to-slf4j - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT software.xdev.tci junit-jupiter-api-support - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT diff --git a/spring-dao-support/pom.xml b/spring-dao-support/pom.xml index 397faa81..25d18c4a 100644 --- a/spring-dao-support/pom.xml +++ b/spring-dao-support/pom.xml @@ -6,7 +6,7 @@ software.xdev.tci spring-dao-support - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT jar spring-dao-support @@ -53,7 +53,7 @@ software.xdev.tci db-jdbc - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT From 7709d9d0dc2d71a47e4af107260a77fe316dfec7 Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 9 Jul 2026 16:36:06 +0200 Subject: [PATCH 078/141] Remove debugging bits --- .github/actions/docker-image-cache/restore/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/docker-image-cache/restore/action.yml b/.github/actions/docker-image-cache/restore/action.yml index 2f160056..80ae7e7d 100644 --- a/.github/actions/docker-image-cache/restore/action.yml +++ b/.github/actions/docker-image-cache/restore/action.yml @@ -41,4 +41,4 @@ runs: printf "%s\n" "${preinstalled_image_ids[@]}" > ~/.cache/docker-preinstalled/ids.txt echo "Importing cache" - time docker load -i ~/.cache/docker/images.tar && echo "Restored docker image cache" && docker image ls || true + docker load -i ~/.cache/docker/images.tar && echo "Restored docker image cache" && docker image ls || true From 86c7eded1e15b527f228840bac3e567473bd8263 Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 9 Jul 2026 16:39:48 +0200 Subject: [PATCH 079/141] Show images for easier debugging --- .github/workflows/run-integration-tests.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/run-integration-tests.yml b/.github/workflows/run-integration-tests.yml index 7680791e..87a0e99c 100644 --- a/.github/workflows/run-integration-tests.yml +++ b/.github/workflows/run-integration-tests.yml @@ -83,6 +83,9 @@ jobs: ACTIONS_RUNTIME_TOKEN: ${{ steps.docker-image-cache-setup-buildx.outputs.ACTIONS_RUNTIME_TOKEN }} ACTIONS_RESULTS_URL: ${{ steps.docker-image-cache-setup-buildx.outputs.ACTIONS_RESULTS_URL }} + - name: Show images + run: docker image ls + - name: Docker Image Cache Save uses: ./.github/actions/docker-image-cache/save with: @@ -146,9 +149,8 @@ jobs: ACTIONS_RUNTIME_TOKEN: ${{ steps.docker-image-cache-setup-buildx.outputs.ACTIONS_RUNTIME_TOKEN }} ACTIONS_RESULTS_URL: ${{ steps.docker-image-cache-setup-buildx.outputs.ACTIONS_RESULTS_URL }} - - name: Display images - run: | - docker image ls + - name: Show images + run: docker image ls - name: Docker Image Cache Save uses: ./.github/actions/docker-image-cache/save From b316500be57b0895615320dc46089826bd64ce0a Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 9 Jul 2026 16:40:20 +0200 Subject: [PATCH 080/141] Speed up test a bit --- .../tci/demo/webapp/cases/LongButLittleResourceUsageTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/advanced-demo/integration-tests/webapp-it/src/test/java/software/xdev/tci/demo/webapp/cases/LongButLittleResourceUsageTest.java b/advanced-demo/integration-tests/webapp-it/src/test/java/software/xdev/tci/demo/webapp/cases/LongButLittleResourceUsageTest.java index 29614c07..e5959b8f 100644 --- a/advanced-demo/integration-tests/webapp-it/src/test/java/software/xdev/tci/demo/webapp/cases/LongButLittleResourceUsageTest.java +++ b/advanced-demo/integration-tests/webapp-it/src/test/java/software/xdev/tci/demo/webapp/cases/LongButLittleResourceUsageTest.java @@ -33,7 +33,7 @@ void simulatedLongTest(final TestBrowser browser) try { - Thread.sleep(20_000); + Thread.sleep(15_000); } catch(final InterruptedException iex) { From cbfb7e5e13c7bfe66b142d09e7c9a43001075796 Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 9 Jul 2026 17:07:06 +0200 Subject: [PATCH 081/141] Demo: Use AOT Should improve image startup speed --- .../integration-tests/tci-webapp/Dockerfile | 45 ++++++- .../containers/WebAppContainerBuilder.java | 119 +++++++++--------- 2 files changed, 103 insertions(+), 61 deletions(-) diff --git a/advanced-demo/integration-tests/tci-webapp/Dockerfile b/advanced-demo/integration-tests/tci-webapp/Dockerfile index e8bb371f..f17232a4 100644 --- a/advanced-demo/integration-tests/tci-webapp/Dockerfile +++ b/advanced-demo/integration-tests/tci-webapp/Dockerfile @@ -74,6 +74,8 @@ RUN mv webapp/target/webapp.jar app.jar \ FROM jre-minimized +ARG ENABLE_AOT + ARG JACOCO_AGENT_ENABLED ARG JACOCO_AGENT_VERSION="0.8.14" @@ -110,9 +112,48 @@ COPY --from=builder --chown=${user}:${group} /builder/extracted/snapshot-depende RUN true COPY --from=builder --chown=${user}:${group} /builder/extracted/application/ ./ +# RUN AOT and boot with dummy configuration +RUN if [[ ! -z "$ENABLE_AOT" ]]; then \ + java \ + -XX:+UseG1GC \ + -XX:+UseCompactObjectHeaders \ + -XX:+UseStringDeduplication \ + -XX:AOTCacheOutput=app.aot \ + -Djava.awt.headless=true \ + -Dspring.context.exit=onRefresh \ + # Disable Flyway + -Dspring.flyway.enabled=false \ + # Disabled DB actuator endpoint + -Dmanagement.health.db.enabled=false \ + # Disbale DB instant connection + -Dspring.jpa.properties.jakarta.persistence.database-product-name=MariaDB \ + -Dspring.jpa.properties.jakarta.persistence.database-major-version=11 \ + -Dspring.jpa.properties.jakarta.persistence.database-minor-version=8 \ + -Dspring.jpa.properties.hibernate.boot.allow.jdbc.metadata.access=false \ + # Fake DB + -Dspring.datasource.url=jdbc:mariadb://localhost:3306/dummy \ + -Dspring.datasource.username=dummy \ + -Dspring.datasource.password=dummy \ + # Fake Auth + -Dspring.security.oauth2.client.registration.local.authorization-grant-type=authorization_code \ + -Dspring.security.oauth2.client.registration.local.redirect-uri="{baseUrl}/{action}/oauth2/code/{registrationId}" \ + -Dspring.security.oauth2.client.provider.local.authorization-uri=http://localhost/connect/authorize \ + -Dspring.security.oauth2.client.provider.local.token-uri=http://localhost/connect/token \ + -Dspring.security.oauth2.client.provider.local.jwk-set-uri=http://localhost/.well-known/openid-configuration/jwks \ + -Dspring.security.oauth2.client.provider.local.user-info-uri=http://localhost/connect/userinfo \ + -Dspring.security.oauth2.client.provider.local.user-info-authentication-method=header \ + -Dspring.security.oauth2.client.provider.local.user-name-attribute=sub \ + -Dspring.security.oauth2.client.registration.local.client-name=Local \ + -Dspring.security.oauth2.client.registration.local.client-id=dummy \ + -Dspring.security.oauth2.client.registration.local.client-secret=dummy \ + -Dspring.security.oauth2.client.registration.local.scope="openid,profile,email,offline_access" \ + -jar app.jar; \ + fi + ENV JAVA_JACOCO_OPTS=${JACOCO_AGENT_ENABLED:+"-javaagent:/jacoco/agent.jar=destfile=/jacoco/report/jacoco.exec"} -ENV JAVA_OPTS="-XX:MaxRAMPercentage=75 -XX:MinHeapFreeRatio=20 -XX:MaxHeapFreeRatio=30 -XX:+UseCompactObjectHeaders -Djava.awt.headless=true" +ENV JAVA_OPTS="-XX:+UseG1GC -XX:MaxRAMPercentage=75 -XX:MinHeapFreeRatio=20 -XX:MaxHeapFreeRatio=30 -XX:+UseCompactObjectHeaders -XX:+UseStringDeduplication -Djava.awt.headless=true" +ENV JAVA_AOT_OPTS=${ENABLE_AOT:+"-XX:AOTCache=app.aot"} EXPOSE 8080 -CMD [ "/bin/sh", "-c", "java $JAVA_OPTS $JAVA_JACOCO_OPTS -jar /opt/app/app.jar" ] +CMD [ "/bin/sh", "-c", "java $JAVA_OPTS $JAVA_JACOCO_OPTS $JAVA_AOT_OPTS -jar /opt/app/app.jar" ] diff --git a/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/containers/WebAppContainerBuilder.java b/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/containers/WebAppContainerBuilder.java index 541d5ca8..2e90ddf6 100644 --- a/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/containers/WebAppContainerBuilder.java +++ b/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/containers/WebAppContainerBuilder.java @@ -29,66 +29,67 @@ public static String getImageName() LOG.info("Building WebApp-DockerImage..."); final NativeAdvancedImageFromDockerfile builder = ImageCreator.nativeImage("tci-demo-webapp") - .withDockerFilePath(Paths.get("../../integration-tests/tci-webapp/Dockerfile")) - .withBaseDir(Paths.get("../../")) - .configureFilesToTransferHandler(h -> h - .withPostGitIgnoreLines( - // Ignore git-folder, as it will be provided in the Dockerfile - ".git/**", - // Ignore other unused folders and extensions - "*.iml", - "*.cmd", - "*.md", - "_dev_infra/**", - "_resource_metrics/**", - // Ignore other Dockerfiles (our required file will always be transferred) - "Dockerfile", - // Ignore not required test-modules that may have changed - // sources only - otherwise the parent pom doesn't find the resources - "integration-tests/**", - "**/src/test/**", - // Ignore resources that are just used for development - "webapp/src/main/resources-dev/**", - // Most files from these folders need to be ignored -> Down there for highest prio - "node_modules", - "target") - // File is in root directory - we can't access it - .withBaseDirRelativeIgnoreFile(null) - .withDockerFileLinesModifier(new DockerfileCOPYParentsEmulator()) - .withTransferArchiveTARCompressorCustomizer(c -> c - // Rewrite parent pom to exclude integration tests - // This way changes in test pom's cause no redownload of dependencies - .withContentModifier(new FileLinesContentModifier() + .withBuildArg("ENABLE_AOT", "1") + .withDockerFilePath(Paths.get("../../integration-tests/tci-webapp/Dockerfile")) + .withBaseDir(Paths.get("../../")) + .configureFilesToTransferHandler(h -> h + .withPostGitIgnoreLines( + // Ignore git-folder, as it will be provided in the Dockerfile + ".git/**", + // Ignore other unused folders and extensions + "*.iml", + "*.cmd", + "*.md", + "_dev_infra/**", + "_resource_metrics/**", + // Ignore other Dockerfiles (our required file will always be transferred) + "Dockerfile", + // Ignore not required test-modules that may have changed + // sources only - otherwise the parent pom doesn't find the resources + "integration-tests/**", + "**/src/test/**", + // Ignore resources that are just used for development + "webapp/src/main/resources-dev/**", + // Most files from these folders need to be ignored -> Down there for highest prio + "node_modules", + "target") + // File is in root directory - we can't access it + .withBaseDirRelativeIgnoreFile(null) + .withDockerFileLinesModifier(new DockerfileCOPYParentsEmulator()) + .withTransferArchiveTARCompressorCustomizer(c -> c + // Rewrite parent pom to exclude integration tests + // This way changes in test pom's cause no redownload of dependencies + .withContentModifier(new FileLinesContentModifier() + { + @Override + public boolean shouldApply( + final Path sourcePath, + final String targetPath, + final TarArchiveEntry tarArchiveEntry) { - @Override - public boolean shouldApply( - final Path sourcePath, - final String targetPath, - final TarArchiveEntry tarArchiveEntry) - { - return "pom.xml".equals(targetPath); - } - - @Override - public List modify( - final List lines, - final Path sourcePath, - final String targetPath, - final TarArchiveEntry tarArchiveEntry) - { - return lines.stream() - // Remove integration tests module - .filter(s -> !s.contains("integration-tests")) - .toList(); - } - - @Override - public boolean isIdentical(final List original, final List created) - { - return original.size() == created.size(); - } - })) - ); + return "pom.xml".equals(targetPath); + } + + @Override + public List modify( + final List lines, + final Path sourcePath, + final String targetPath, + final TarArchiveEntry tarArchiveEntry) + { + return lines.stream() + // Remove integration tests module + .filter(s -> !s.contains("integration-tests")) + .toList(); + } + + @Override + public boolean isIdentical(final List original, final List created) + { + return original.size() == created.size(); + } + })) + ); if(JaCoCoConfig.instance().enabled()) { From 6507b51718c448aebda3c7413f998f7844312413 Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 9 Jul 2026 17:13:50 +0200 Subject: [PATCH 082/141] Try fix AOT error --- advanced-demo/integration-tests/tci-webapp/Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/advanced-demo/integration-tests/tci-webapp/Dockerfile b/advanced-demo/integration-tests/tci-webapp/Dockerfile index f17232a4..e309ad3e 100644 --- a/advanced-demo/integration-tests/tci-webapp/Dockerfile +++ b/advanced-demo/integration-tests/tci-webapp/Dockerfile @@ -119,6 +119,7 @@ RUN if [[ ! -z "$ENABLE_AOT" ]]; then \ -XX:+UseCompactObjectHeaders \ -XX:+UseStringDeduplication \ -XX:AOTCacheOutput=app.aot \ + ${JACOCO_AGENT_ENABLED:+"-javaagent:/jacoco/agent.jar=destfile=/jacoco/report/jacoco.exec"} \ -Djava.awt.headless=true \ -Dspring.context.exit=onRefresh \ # Disable Flyway From 88ede5bf3057ebe341e96d3818c4f05d1d268079 Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 9 Jul 2026 17:22:27 +0200 Subject: [PATCH 083/141] Revert "Try fix AOT error" This reverts commit 6507b51718c448aebda3c7413f998f7844312413. --- advanced-demo/integration-tests/tci-webapp/Dockerfile | 1 - 1 file changed, 1 deletion(-) diff --git a/advanced-demo/integration-tests/tci-webapp/Dockerfile b/advanced-demo/integration-tests/tci-webapp/Dockerfile index e309ad3e..f17232a4 100644 --- a/advanced-demo/integration-tests/tci-webapp/Dockerfile +++ b/advanced-demo/integration-tests/tci-webapp/Dockerfile @@ -119,7 +119,6 @@ RUN if [[ ! -z "$ENABLE_AOT" ]]; then \ -XX:+UseCompactObjectHeaders \ -XX:+UseStringDeduplication \ -XX:AOTCacheOutput=app.aot \ - ${JACOCO_AGENT_ENABLED:+"-javaagent:/jacoco/agent.jar=destfile=/jacoco/report/jacoco.exec"} \ -Djava.awt.headless=true \ -Dspring.context.exit=onRefresh \ # Disable Flyway From a04d4b6fcf22cc0345da6865b94ff5289f340e76 Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 9 Jul 2026 17:30:56 +0200 Subject: [PATCH 084/141] Update run-integration-tests.yml --- .github/workflows/run-integration-tests.yml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/run-integration-tests.yml b/.github/workflows/run-integration-tests.yml index 87a0e99c..b34b0566 100644 --- a/.github/workflows/run-integration-tests.yml +++ b/.github/workflows/run-integration-tests.yml @@ -100,6 +100,11 @@ jobs: parallel: [0, 2] pre-start: [false, true] java: [21] + # Only enable jacoco on the fastest builds because AOT is not fully working with it + include: + - project: webapp-it + parallel: 2 + jacoco: [true] name: "selenium (${{matrix.project}}, ${{matrix.parallel}}, ${{matrix.pre-start}}, ${{matrix.java}})" runs-on: ubuntu-latest if: ${{ !(github.event_name == 'pull_request' && startsWith(github.head_ref, 'renovate/')) }} @@ -139,7 +144,7 @@ jobs: run: | ./mvnw -B test \ -pl "advanced-demo/integration-tests/${{ matrix.project }}" -am \ - -P run-it,jacoco \ + -P run-it${{ matrix.jacoco && ',jacoco' || '' }} \ -Dtci.image-build.delete-on-exit=true \ -Dtci.image-build.cache-from="type=gha,scope=§image" \ -Dtci.image-build.cache-to="type=gha,mode=max,scope=§image" \ @@ -166,16 +171,15 @@ jobs: name: test-fail-videos-${{ matrix.java }}-${{ matrix.project }}-${{ matrix.parallel }}-${{ matrix.pre-start }} path: advanced-demo/integration-tests/${{ matrix.project }}/target/records if-no-files-found: ignore - background: true - name: Aggregate WebApp JaCoCo report - if: ${{ success() }} + if: ${{ matrix.jacoco && success() }} # Important: Also run compile (if not already done) so that we have all class files - otherwise the report will be incomplete run: | ./mvnw -B compile jacoco-aggregator:report-aggregate-all -pl "advanced-demo/webapp" -am -T2C - name: Upload WebApp JaCoCo report - if: ${{ success() }} + if: ${{ matrix.jacoco && success() }} uses: actions/upload-artifact@v7 with: name: webapp-jacoco-report-${{ matrix.java }}-${{ matrix.project }}-${{ matrix.parallel }}-${{ matrix.pre-start }} From 917078a5f8597df1d27cb264f488033f6279fe7a Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 9 Jul 2026 17:42:43 +0200 Subject: [PATCH 085/141] Docs --- .../tci/demo/tci/webapp/containers/WebAppContainerBuilder.java | 1 + 1 file changed, 1 insertion(+) diff --git a/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/containers/WebAppContainerBuilder.java b/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/containers/WebAppContainerBuilder.java index 2e90ddf6..5eb7fe74 100644 --- a/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/containers/WebAppContainerBuilder.java +++ b/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/containers/WebAppContainerBuilder.java @@ -29,6 +29,7 @@ public static String getImageName() LOG.info("Building WebApp-DockerImage..."); final NativeAdvancedImageFromDockerfile builder = ImageCreator.nativeImage("tci-demo-webapp") + // NOTE: AOT can't be used properly when JaCoCo is active .withBuildArg("ENABLE_AOT", "1") .withDockerFilePath(Paths.get("../../integration-tests/tci-webapp/Dockerfile")) .withBaseDir(Paths.get("../../")) From d63c00b8a676f0eb05f6180e0c8621f98f539367 Mon Sep 17 00:00:00 2001 From: AB Date: Fri, 10 Jul 2026 08:45:16 +0200 Subject: [PATCH 086/141] Improve pre-installed image handling --- .../docker-image-cache/restore/action.yml | 17 ++++++++++------- .../actions/docker-image-cache/save/action.yml | 13 ++++++++++--- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/.github/actions/docker-image-cache/restore/action.yml b/.github/actions/docker-image-cache/restore/action.yml index 80ae7e7d..528c714e 100644 --- a/.github/actions/docker-image-cache/restore/action.yml +++ b/.github/actions/docker-image-cache/restore/action.yml @@ -8,6 +8,9 @@ inputs: restore-keys: description: 'An ordered multiline string listing the prefix-matched keys, that are used for restoring stale cache if no cache hit occurred for key. Note `cache-hit` returns false in this case.' required: false + save-preinstalled-images: + description: 'Should pre-installed images be saved (not recommended)' + required: false outputs: key: description: 'Identical to the input - can be used to prevent copy pasting' @@ -18,8 +21,6 @@ outputs: runs: using: "composite" steps: - # RESTORE "NORMAL" DOCKER IMAGE CACHE - # NOTE 2026-07-09 Parallel execution is not yet available for composite actions # https://github.blog/changelog/2026-06-25-actions-steps-can-now-be-run-in-parallel/ - name: Restore docker image cache @@ -33,12 +34,14 @@ runs: - name: Load docker image cache shell: bash run: | - echo "Getting preinstalled images" && docker image ls - preinstalled_image_ids=$(docker images -q | sort) + if [ "${{ inputs.save-preinstalled-images }}" != true ]; then + echo "Getting preinstalled images" && docker image ls + preinstalled_image_ids=$(docker images -q | sort) - echo "Storing preinstalled image ids" && echo $preinstalled_image_ids - mkdir -p ~/.cache/docker-preinstalled - printf "%s\n" "${preinstalled_image_ids[@]}" > ~/.cache/docker-preinstalled/ids.txt + echo "Storing preinstalled image ids" && echo $preinstalled_image_ids + mkdir -p ~/.cache/docker-preinstalled + printf "%s\n" "${preinstalled_image_ids[@]}" > ~/.cache/docker-preinstalled/ids-${{ github.run_id }}.txt + fi echo "Importing cache" docker load -i ~/.cache/docker/images.tar && echo "Restored docker image cache" && docker image ls || true diff --git a/.github/actions/docker-image-cache/save/action.yml b/.github/actions/docker-image-cache/save/action.yml index d9e3b341..4d21716d 100644 --- a/.github/actions/docker-image-cache/save/action.yml +++ b/.github/actions/docker-image-cache/save/action.yml @@ -20,19 +20,26 @@ runs: docker image prune -f echo "Currently available images" && docker image ls - if [ -f ~/.cache/docker-preinstalled/ids.txt ]; then - echo && echo "Will NOT cache pre-installed images" && cat ~/.cache/docker-preinstalled/ids.txt + has_preinstalled_ids=false + if [ -f ~/.cache/docker-preinstalled/ids-${{ github.run_id }}.txt ]; then + echo && echo "Will NOT cache pre-installed images" && cat ~/.cache/docker-preinstalled/ids-${{ github.run_id }}.txt + has_preinstalled_ids=true fi echo && echo "Will NOT cache images" && docker image ls ${CI_CACHE_DOCKER_SAVE_IGNORE_FILTERS:--f="label=org.testcontainers.sessionId"} mkdir -p ~/.cache/docker image_ids_to_save=$(comm -23 <(docker images -q | sort) <(docker image ls -q ${CI_CACHE_DOCKER_SAVE_IGNORE_FILTERS:--f="label=org.testcontainers.sessionId"} | sort)) - image_ids_to_save=$(comm -23 <(echo "${image_ids_to_save[@]}") <(cat ~/.cache/docker-preinstalled/ids.txt || echo "")) + if [ "$has_preinstalled_ids" = true ]; then + image_ids_to_save=$(comm -23 <(echo "${image_ids_to_save[@]}") <(cat ~/.cache/docker-preinstalled/ids-${{ github.run_id }}.txt || echo "")) + fi echo && echo "Saving image ids" && echo $image_ids_to_save docker save $image_ids_to_save -o ~/.cache/docker/images.tar ls -lha ~/.cache/docker + if [ "$has_preinstalled_ids" = true ]; then + rm -f ~/.cache/docker-preinstalled/ids-${{ github.run_id }}.txt || true + fi - name: Save docker image cache if: inputs.cache-hit != 'true' From aded27f213ea3cac4bcd6713e7deaf215795561d Mon Sep 17 00:00:00 2001 From: AB Date: Fri, 10 Jul 2026 09:17:52 +0200 Subject: [PATCH 087/141] Update JaCoCo agent to latest version --- advanced-demo/integration-tests/tci-webapp/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/advanced-demo/integration-tests/tci-webapp/Dockerfile b/advanced-demo/integration-tests/tci-webapp/Dockerfile index f17232a4..66125eee 100644 --- a/advanced-demo/integration-tests/tci-webapp/Dockerfile +++ b/advanced-demo/integration-tests/tci-webapp/Dockerfile @@ -77,7 +77,7 @@ FROM jre-minimized ARG ENABLE_AOT ARG JACOCO_AGENT_ENABLED -ARG JACOCO_AGENT_VERSION="0.8.14" +ARG JACOCO_AGENT_VERSION="0.8.15" ARG user=app ARG group=app From a53b7bc1dcd9861addc77b317942663953000ec9 Mon Sep 17 00:00:00 2001 From: AB Date: Fri, 10 Jul 2026 09:18:00 +0200 Subject: [PATCH 088/141] Docs --- .github/workflows/run-integration-tests.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/run-integration-tests.yml b/.github/workflows/run-integration-tests.yml index b34b0566..c2aac9ec 100644 --- a/.github/workflows/run-integration-tests.yml +++ b/.github/workflows/run-integration-tests.yml @@ -63,6 +63,9 @@ jobs: id: docker-image-cache-restore uses: ./.github/actions/docker-image-cache/restore with: + # This cache could always change however because docker images can be changed remotely if the version is not fixated + # As a compromise it utilizes the hash of the pom files because when the dependencies stay the same + # the images should also be roughly the same key: it-docker-images-persistence-${{ matrix.project }}-${{ hashFiles('**/pom.xml') }} restore-keys: | ${{ runner.os }}-it-docker-images-persistence-${{ matrix.project }}-${{ hashFiles('**/pom.xml') }} @@ -131,6 +134,9 @@ jobs: id: docker-image-cache-restore uses: ./.github/actions/docker-image-cache/restore with: + # This cache could always change however because docker images can be changed remotely if the version is not fixated + # As a compromise it utilizes the hash of the pom files because when the dependencies stay the same + # the images should also be roughly the same key: it-docker-images-selenium-${{ matrix.project }}-${{ hashFiles('**/pom.xml') }} restore-keys: | ${{ runner.os }}-it-docker-images-selenium-${{ matrix.project }}-${{ hashFiles('**/pom.xml') }} From bbb0369eadcdba11921bf721bd6205c254eb9903 Mon Sep 17 00:00:00 2001 From: AB Date: Fri, 10 Jul 2026 09:22:15 +0200 Subject: [PATCH 089/141] Add dedicated JaCoCo test run --- .github/workflows/run-integration-tests.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run-integration-tests.yml b/.github/workflows/run-integration-tests.yml index c2aac9ec..9a02e7ef 100644 --- a/.github/workflows/run-integration-tests.yml +++ b/.github/workflows/run-integration-tests.yml @@ -103,11 +103,14 @@ jobs: parallel: [0, 2] pre-start: [false, true] java: [21] - # Only enable jacoco on the fastest builds because AOT is not fully working with it + jacoco: [false] + # Dedicated build with JaCoCo include: - project: webapp-it parallel: 2 - jacoco: [true] + pre-start: true + java: 21 + jacoco: true name: "selenium (${{matrix.project}}, ${{matrix.parallel}}, ${{matrix.pre-start}}, ${{matrix.java}})" runs-on: ubuntu-latest if: ${{ !(github.event_name == 'pull_request' && startsWith(github.head_ref, 'renovate/')) }} From 7b91e37e0b2f29fdceff37cd22bab9bab6b1793e Mon Sep 17 00:00:00 2001 From: AB Date: Fri, 10 Jul 2026 09:24:44 +0200 Subject: [PATCH 090/141] Update run-integration-tests.yml --- .github/workflows/run-integration-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-integration-tests.yml b/.github/workflows/run-integration-tests.yml index 9a02e7ef..bb9179ee 100644 --- a/.github/workflows/run-integration-tests.yml +++ b/.github/workflows/run-integration-tests.yml @@ -111,7 +111,7 @@ jobs: pre-start: true java: 21 jacoco: true - name: "selenium (${{matrix.project}}, ${{matrix.parallel}}, ${{matrix.pre-start}}, ${{matrix.java}})" + name: "selenium (${{matrix.project}}, ${{matrix.parallel }}, ${{matrix.pre-start}}, ${{matrix.java}}${{ matrix.jacoco && ', JaCoCo' }})" runs-on: ubuntu-latest if: ${{ !(github.event_name == 'pull_request' && startsWith(github.head_ref, 'renovate/')) }} timeout-minutes: 30 From 2b35ffb025f66af62977b4895e8f11164a585783 Mon Sep 17 00:00:00 2001 From: AB Date: Fri, 10 Jul 2026 09:26:16 +0200 Subject: [PATCH 091/141] Add different build cache scope when jacoco is active --- .github/workflows/run-integration-tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run-integration-tests.yml b/.github/workflows/run-integration-tests.yml index bb9179ee..3250246c 100644 --- a/.github/workflows/run-integration-tests.yml +++ b/.github/workflows/run-integration-tests.yml @@ -155,8 +155,8 @@ jobs: -pl "advanced-demo/integration-tests/${{ matrix.project }}" -am \ -P run-it${{ matrix.jacoco && ',jacoco' || '' }} \ -Dtci.image-build.delete-on-exit=true \ - -Dtci.image-build.cache-from="type=gha,scope=§image" \ - -Dtci.image-build.cache-to="type=gha,mode=max,scope=§image" \ + -Dtci.image-build.cache-from="type=gha,scope=§image${{ matrix.jacoco && '-jacoco' || '' }}" \ + -Dtci.image-build.cache-to="type=gha,mode=max,scope=§image${{ matrix.jacoco && '-jacoco' || '' }}" \ ${{ matrix.pre-start && '-Dinfra-pre-start.enabled=1 ' || '' }} \ ${{ matrix.parallel > 0 && format('-Djunit.jupiter.execution.parallel.enabled=true -Djunit.jupiter.execution.parallel.mode.default=concurrent -Djunit.jupiter.execution.parallel.mode.classes.default=concurrent -Djunit.jupiter.execution.parallel.config.strategy=fixed -Djunit.jupiter.execution.parallel.config.fixed.parallelism=2 -Djunit.jupiter.execution.parallel.config.fixed.max-pool-size={0} ', matrix.parallel) || '' }} env: From 5f70d8f18dfcc5d7bd0522a0befc43606071fa0c Mon Sep 17 00:00:00 2001 From: AB Date: Fri, 10 Jul 2026 09:31:06 +0200 Subject: [PATCH 092/141] Fix display --- .github/workflows/run-integration-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-integration-tests.yml b/.github/workflows/run-integration-tests.yml index 3250246c..fd0ea1ce 100644 --- a/.github/workflows/run-integration-tests.yml +++ b/.github/workflows/run-integration-tests.yml @@ -111,7 +111,7 @@ jobs: pre-start: true java: 21 jacoco: true - name: "selenium (${{matrix.project}}, ${{matrix.parallel }}, ${{matrix.pre-start}}, ${{matrix.java}}${{ matrix.jacoco && ', JaCoCo' }})" + name: "selenium (${{matrix.project}}, ${{matrix.parallel }}, ${{matrix.pre-start}}, ${{matrix.java}}${{ matrix.jacoco && ', JaCoCo' || '' }})" runs-on: ubuntu-latest if: ${{ !(github.event_name == 'pull_request' && startsWith(github.head_ref, 'renovate/')) }} timeout-minutes: 30 From b31eb4fef83348cce283031ae4fe9262eff9a02d Mon Sep 17 00:00:00 2001 From: XDEV Renovate Bot Date: Fri, 10 Jul 2026 07:38:32 +0000 Subject: [PATCH 093/141] Update lycheeverse/lychee-action digest to e747777 --- .github/workflows/broken-links.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/broken-links.yml b/.github/workflows/broken-links.yml index ce623e98..afdffd67 100644 --- a/.github/workflows/broken-links.yml +++ b/.github/workflows/broken-links.yml @@ -18,7 +18,7 @@ jobs: - name: Link Checker id: lychee - uses: lycheeverse/lychee-action@8646ba30535128ac92d33dfc9133794bfdd9b411 # v2 + uses: lycheeverse/lychee-action@e7477775783ea5526144ba13e8db5eec57747ce8 # v2 with: args: "--verbose --no-progress './**/*.md'" fail: false # Don't fail on broken links, create an issue instead From 3ca0f9a70f878a4b039027e4edfd2292c9f381f2 Mon Sep 17 00:00:00 2001 From: XDEV Renovate Bot Date: Fri, 10 Jul 2026 07:44:39 +0000 Subject: [PATCH 094/141] Update lycheeverse/lychee-action digest to e747777 --- .github/workflows/broken-links.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/broken-links.yml b/.github/workflows/broken-links.yml index fbe05e74..8aeed096 100644 --- a/.github/workflows/broken-links.yml +++ b/.github/workflows/broken-links.yml @@ -19,7 +19,7 @@ jobs: - name: Link Checker id: lychee - uses: lycheeverse/lychee-action@8646ba30535128ac92d33dfc9133794bfdd9b411 # v2 + uses: lycheeverse/lychee-action@e7477775783ea5526144ba13e8db5eec57747ce8 # v2 with: args: "--verbose --no-progress './**/*.md'" fail: false # Don't fail on broken links, create an issue instead From c23918a4562637634aef26c653f57d1c4c9d9dc5 Mon Sep 17 00:00:00 2001 From: XDEV Renovate Bot Date: Fri, 10 Jul 2026 07:44:42 +0000 Subject: [PATCH 095/141] Update dependency ch.qos.logback:logback-classic to v1.5.38 --- base-demo/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base-demo/pom.xml b/base-demo/pom.xml index d755ccd3..726def11 100644 --- a/base-demo/pom.xml +++ b/base-demo/pom.xml @@ -45,7 +45,7 @@ ch.qos.logback logback-classic - 1.5.37 + 1.5.38 test From 7c8b528ba356fc6a67e6750875139ff303cce852 Mon Sep 17 00:00:00 2001 From: AB Date: Fri, 10 Jul 2026 12:10:30 +0200 Subject: [PATCH 096/141] Save cache in background --- .../tci/db/containers/DBContainerBuilder.java | 38 ++-- .../containers/WebAppContainerBuilder.java | 157 ++++++++--------- image-build/README.md | 2 + image-build/pom.xml | 2 +- .../{ImageCreator.java => BuildImage.java} | 25 ++- ...ageHandler.java => BuildImageHandler.java} | 15 +- .../imagebuild/DefaultBuildImageHandler.java | 164 ++++++++++++++++++ .../imagebuild/DefaultCreateImageHandler.java | 87 ---------- .../cache/async/TCICacheSaveRegistry.java | 50 ++++++ .../cache/async/TCIWaitForCacheSave.java | 69 ++++++++ ...nfig.java => BuildImageHandlerConfig.java} | 10 +- ...va => DefaultBuildImageHandlerConfig.java} | 28 ++- ...it.platform.launcher.TestExecutionListener | 1 + ...ware.xdev.tci.imagebuild.BuildImageHandler | 1 + ...are.xdev.tci.imagebuild.CreateImageHandler | 1 - ....imagebuild.config.BuildImageHandlerConfig | 1 + ...imagebuild.config.CreateImageHandlerConfig | 1 - 17 files changed, 441 insertions(+), 211 deletions(-) rename image-build/src/main/java/software/xdev/tci/imagebuild/{ImageCreator.java => BuildImage.java} (58%) rename image-build/src/main/java/software/xdev/tci/imagebuild/{CreateImageHandler.java => BuildImageHandler.java} (71%) create mode 100644 image-build/src/main/java/software/xdev/tci/imagebuild/DefaultBuildImageHandler.java delete mode 100644 image-build/src/main/java/software/xdev/tci/imagebuild/DefaultCreateImageHandler.java create mode 100644 image-build/src/main/java/software/xdev/tci/imagebuild/cache/async/TCICacheSaveRegistry.java create mode 100644 image-build/src/main/java/software/xdev/tci/imagebuild/cache/async/TCIWaitForCacheSave.java rename image-build/src/main/java/software/xdev/tci/imagebuild/config/{CreateImageHandlerConfig.java => BuildImageHandlerConfig.java} (78%) rename image-build/src/main/java/software/xdev/tci/imagebuild/config/{DefaultCreateImageHandlerConfig.java => DefaultBuildImageHandlerConfig.java} (69%) create mode 100644 image-build/src/main/resources/META-INF/services/org.junit.platform.launcher.TestExecutionListener create mode 100644 image-build/src/main/resources/META-INF/services/software.xdev.tci.imagebuild.BuildImageHandler delete mode 100644 image-build/src/main/resources/META-INF/services/software.xdev.tci.imagebuild.CreateImageHandler create mode 100644 image-build/src/main/resources/META-INF/services/software.xdev.tci.imagebuild.config.BuildImageHandlerConfig delete mode 100644 image-build/src/main/resources/META-INF/services/software.xdev.tci.imagebuild.config.CreateImageHandlerConfig diff --git a/advanced-demo/integration-tests/tci-db/src/main/java/software/xdev/tci/demo/tci/db/containers/DBContainerBuilder.java b/advanced-demo/integration-tests/tci-db/src/main/java/software/xdev/tci/demo/tci/db/containers/DBContainerBuilder.java index e05bce43..de78f3e2 100644 --- a/advanced-demo/integration-tests/tci-db/src/main/java/software/xdev/tci/demo/tci/db/containers/DBContainerBuilder.java +++ b/advanced-demo/integration-tests/tci-db/src/main/java/software/xdev/tci/demo/tci/db/containers/DBContainerBuilder.java @@ -3,39 +3,29 @@ import java.nio.file.Paths; import java.time.Duration; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import software.xdev.tci.imagebuild.ImageCreator; -import software.xdev.testcontainers.imagebuilder.buildxnative.NativeAdvancedImageFromDockerfile; +import software.xdev.tci.imagebuild.BuildImage; public final class DBContainerBuilder { - private static final Logger LOG = LoggerFactory.getLogger(DBContainerBuilder.class); - private DBContainerBuilder() { } public static String getImageName() { - LOG.info("Building Webapp-db-DockerImage..."); - - final NativeAdvancedImageFromDockerfile builder = ImageCreator.nativeImage("tci-demo-db") - .withDockerFilePath(Paths.get("../tci-db/Dockerfile")) - .withBaseDir(Paths.get("../")) - .configureFilesToTransferHandler(h -> h - .withPostGitIgnoreLines( - // Ignore everything - "**") - .withBaseDirRelativeIgnoreFile(null) - ); - - final String name = builder.build(Duration.ofMinutes(5)); - - LOG.info("Built Image; Name ='{}'", name); - - return name; + return BuildImage.nativeImage( + "tci-demo-db", + Duration.ofMinutes(5), + builder -> builder + .withDockerFilePath(Paths.get("../tci-db/Dockerfile")) + .withBaseDir(Paths.get("../")) + .configureFilesToTransferHandler(h -> h + .withPostGitIgnoreLines( + // Ignore everything + "**") + .withBaseDirRelativeIgnoreFile(null) + ) + ); } } diff --git a/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/containers/WebAppContainerBuilder.java b/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/containers/WebAppContainerBuilder.java index 5eb7fe74..02f6c01f 100644 --- a/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/containers/WebAppContainerBuilder.java +++ b/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/containers/WebAppContainerBuilder.java @@ -6,101 +6,94 @@ import java.util.List; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import software.xdev.tci.imagebuild.ImageCreator; +import software.xdev.tci.imagebuild.BuildImage; import software.xdev.tci.jacoco.testbase.config.JaCoCoConfig; -import software.xdev.testcontainers.imagebuilder.buildxnative.NativeAdvancedImageFromDockerfile; import software.xdev.testcontainers.imagebuilder.compat.DockerfileCOPYParentsEmulator; import software.xdev.testcontainers.imagebuilder.transfer.fcm.FileLinesContentModifier; public final class WebAppContainerBuilder { - private static final Logger LOG = LoggerFactory.getLogger(WebAppContainerBuilder.class); - private WebAppContainerBuilder() { } public static String getImageName() { - LOG.info("Building WebApp-DockerImage..."); - - final NativeAdvancedImageFromDockerfile builder = ImageCreator.nativeImage("tci-demo-webapp") - // NOTE: AOT can't be used properly when JaCoCo is active - .withBuildArg("ENABLE_AOT", "1") - .withDockerFilePath(Paths.get("../../integration-tests/tci-webapp/Dockerfile")) - .withBaseDir(Paths.get("../../")) - .configureFilesToTransferHandler(h -> h - .withPostGitIgnoreLines( - // Ignore git-folder, as it will be provided in the Dockerfile - ".git/**", - // Ignore other unused folders and extensions - "*.iml", - "*.cmd", - "*.md", - "_dev_infra/**", - "_resource_metrics/**", - // Ignore other Dockerfiles (our required file will always be transferred) - "Dockerfile", - // Ignore not required test-modules that may have changed - // sources only - otherwise the parent pom doesn't find the resources - "integration-tests/**", - "**/src/test/**", - // Ignore resources that are just used for development - "webapp/src/main/resources-dev/**", - // Most files from these folders need to be ignored -> Down there for highest prio - "node_modules", - "target") - // File is in root directory - we can't access it - .withBaseDirRelativeIgnoreFile(null) - .withDockerFileLinesModifier(new DockerfileCOPYParentsEmulator()) - .withTransferArchiveTARCompressorCustomizer(c -> c - // Rewrite parent pom to exclude integration tests - // This way changes in test pom's cause no redownload of dependencies - .withContentModifier(new FileLinesContentModifier() - { - @Override - public boolean shouldApply( - final Path sourcePath, - final String targetPath, - final TarArchiveEntry tarArchiveEntry) - { - return "pom.xml".equals(targetPath); - } - - @Override - public List modify( - final List lines, - final Path sourcePath, - final String targetPath, - final TarArchiveEntry tarArchiveEntry) - { - return lines.stream() - // Remove integration tests module - .filter(s -> !s.contains("integration-tests")) - .toList(); - } - - @Override - public boolean isIdentical(final List original, final List created) - { - return original.size() == created.size(); - } - })) - ); - - if(JaCoCoConfig.instance().enabled()) - { - builder.withBuildArg("JACOCO_AGENT_ENABLED", "1"); - } - - final String imageName = builder.build(Duration.ofMinutes(5)); - - LOG.info("Built Image; Name ='{}'", imageName); - - return imageName; + return BuildImage.nativeImage( + "tci-demo-webapp", + Duration.ofMinutes(5), + builder -> { + builder + // NOTE: AOT can't be used properly when JaCoCo is active + .withBuildArg("ENABLE_AOT", "1") + .withDockerFilePath(Paths.get("../../integration-tests/tci-webapp/Dockerfile")) + .withBaseDir(Paths.get("../../")) + .configureFilesToTransferHandler(h -> h + .withPostGitIgnoreLines( + // Ignore git-folder, as it will be provided in the Dockerfile + ".git/**", + // Ignore other unused folders and extensions + "*.iml", + "*.cmd", + "*.md", + "_dev_infra/**", + "_resource_metrics/**", + // Ignore other Dockerfiles (our required file will always be transferred) + "Dockerfile", + // Ignore not required test-modules that may have changed + // sources only - otherwise the parent pom doesn't find the resources + "integration-tests/**", + "**/src/test/**", + // Ignore resources that are just used for development + "webapp/src/main/resources-dev/**", + // Most files from these folders need to be ignored -> Down there for highest prio + "node_modules", + "target") + // File is in root directory - we can't access it + .withBaseDirRelativeIgnoreFile(null) + .withDockerFileLinesModifier(new DockerfileCOPYParentsEmulator()) + .withTransferArchiveTARCompressorCustomizer(c -> c + // Rewrite parent pom to exclude integration tests + // This way changes in test pom's cause no redownload of dependencies + .withContentModifier(new FileLinesContentModifier() + { + @Override + public boolean shouldApply( + final Path sourcePath, + final String targetPath, + final TarArchiveEntry tarArchiveEntry) + { + return "pom.xml".equals(targetPath); + } + + @Override + public List modify( + final List lines, + final Path sourcePath, + final String targetPath, + final TarArchiveEntry tarArchiveEntry) + { + return lines.stream() + // Remove integration tests module + .filter(s -> !s.contains("integration-tests")) + .toList(); + } + + @Override + public boolean isIdentical(final List original, final List created) + { + return original.size() == created.size(); + } + }))); + + if(JaCoCoConfig.instance().enabled()) + { + builder.withBuildArg("JACOCO_AGENT_ENABLED", "1"); + } + + return builder; + }); } } diff --git a/image-build/README.md b/image-build/README.md index 1d81d786..12c4a9c2 100644 --- a/image-build/README.md +++ b/image-build/README.md @@ -25,6 +25,8 @@ _You should know exactly what you're doing when doing modifications._ | `logger-for-build-prefix` | `string` | `container.build.` | Prefix used for the build logger | | `cache-from` | `string` | - | Only applies to BuildKit (native) build.
See [Docker docs](https://docs.docker.com/build/cache/backends/) for details. | | `cache-to` | `string` | - | Only applies to BuildKit (native) build.
See [Docker docs](https://docs.docker.com/build/cache/backends/) for details. | +| `save-cache-in-background` | `bool` | if `cache-to` set `true`
otherwise `false` | Builds the image the first time WITHOUT `cache-to` and the async in the background again WITH `cache-to`. This way saving the cache does not delay test execution. | +| `wait-for-save-cache-in-background` | `bool` | `true` | Wait until all caches are saved before terminating | diff --git a/image-build/pom.xml b/image-build/pom.xml index 093e6939..ba656ab9 100644 --- a/image-build/pom.xml +++ b/image-build/pom.xml @@ -58,7 +58,7 @@ software.xdev testcontainers-advanced-imagebuilder - 4.0.0 + 4.1.0-SNAPSHOT diff --git a/image-build/src/main/java/software/xdev/tci/imagebuild/ImageCreator.java b/image-build/src/main/java/software/xdev/tci/imagebuild/BuildImage.java similarity index 58% rename from image-build/src/main/java/software/xdev/tci/imagebuild/ImageCreator.java rename to image-build/src/main/java/software/xdev/tci/imagebuild/BuildImage.java index ef452c0d..eb385204 100644 --- a/image-build/src/main/java/software/xdev/tci/imagebuild/ImageCreator.java +++ b/image-build/src/main/java/software/xdev/tci/imagebuild/BuildImage.java @@ -15,29 +15,38 @@ */ package software.xdev.tci.imagebuild; +import java.time.Duration; +import java.util.function.UnaryOperator; + import software.xdev.tci.serviceloading.TCIServiceLoaderHolder; import software.xdev.testcontainers.imagebuilder.AdvancedImageFromDockerFile; import software.xdev.testcontainers.imagebuilder.buildxnative.NativeAdvancedImageFromDockerfile; -public final class ImageCreator +public final class BuildImage { - public static NativeAdvancedImageFromDockerfile nativeImage(final String dockerImage) + public static String nativeImage( + final String dockerImage, + final Duration timeout, + final UnaryOperator configure) { - return impl().nativeImage(dockerImage); + return impl().nativeImage(dockerImage, timeout, configure); } - public static AdvancedImageFromDockerFile image(final String dockerImage) + public static String image( + final String dockerImage, + final Duration timeout, + final UnaryOperator configure) { - return impl().image(dockerImage); + return impl().image(dockerImage, timeout, configure); } - public static CreateImageHandler impl() + public static BuildImageHandler impl() { - return TCIServiceLoaderHolder.instance().service(CreateImageHandler.class); + return TCIServiceLoaderHolder.instance().service(BuildImageHandler.class); } - private ImageCreator() + private BuildImage() { } } diff --git a/image-build/src/main/java/software/xdev/tci/imagebuild/CreateImageHandler.java b/image-build/src/main/java/software/xdev/tci/imagebuild/BuildImageHandler.java similarity index 71% rename from image-build/src/main/java/software/xdev/tci/imagebuild/CreateImageHandler.java rename to image-build/src/main/java/software/xdev/tci/imagebuild/BuildImageHandler.java index b447f6c7..cf08dc96 100644 --- a/image-build/src/main/java/software/xdev/tci/imagebuild/CreateImageHandler.java +++ b/image-build/src/main/java/software/xdev/tci/imagebuild/BuildImageHandler.java @@ -15,13 +15,22 @@ */ package software.xdev.tci.imagebuild; +import java.time.Duration; +import java.util.function.UnaryOperator; + import software.xdev.testcontainers.imagebuilder.AdvancedImageFromDockerFile; import software.xdev.testcontainers.imagebuilder.buildxnative.NativeAdvancedImageFromDockerfile; -public interface CreateImageHandler +public interface BuildImageHandler { - NativeAdvancedImageFromDockerfile nativeImage(String dockerImage); + String nativeImage( + String dockerImage, + Duration timeout, + UnaryOperator configure); - AdvancedImageFromDockerFile image(String dockerImage); + String image( + String dockerImage, + Duration timeout, + UnaryOperator configure); } diff --git a/image-build/src/main/java/software/xdev/tci/imagebuild/DefaultBuildImageHandler.java b/image-build/src/main/java/software/xdev/tci/imagebuild/DefaultBuildImageHandler.java new file mode 100644 index 00000000..589ab297 --- /dev/null +++ b/image-build/src/main/java/software/xdev/tci/imagebuild/DefaultBuildImageHandler.java @@ -0,0 +1,164 @@ +/* + * Copyright © 2025 XDEV Software (https://xdev.software) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package software.xdev.tci.imagebuild; + +import java.time.Duration; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.function.UnaryOperator; +import java.util.regex.Pattern; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import software.xdev.tci.concurrent.TCIExecutorServiceHolder; +import software.xdev.tci.imagebuild.cache.async.TCICacheSaveRegistry; +import software.xdev.tci.imagebuild.config.BuildImageHandlerConfig; +import software.xdev.testcontainers.imagebuilder.AbstractImageFromDockerfile; +import software.xdev.testcontainers.imagebuilder.AdvancedImageFromDockerFile; +import software.xdev.testcontainers.imagebuilder.buildxnative.NativeAdvancedImageFromDockerfile; + + +public class DefaultBuildImageHandler implements BuildImageHandler +{ + private static final Logger LOG = LoggerFactory.getLogger(DefaultBuildImageHandler.class); + + protected static final Pattern DOCKER_IMAGE_SANITIZATION_PATTERN = + Pattern.compile("[^A-Za-z0-9-_]"); + + protected final BuildImageHandlerConfig config; + + public DefaultBuildImageHandler() + { + this(BuildImageHandlerConfig.instance()); + } + + public DefaultBuildImageHandler(final BuildImageHandlerConfig config) + { + this.config = config; + } + + @Override + public String nativeImage( + final String dockerImage, + final Duration timeout, + final UnaryOperator configure) + { + LOG.info("Starting build of (native) image {}", dockerImage); + + final String sanitizeDockerImageName = this.sanitizeDockerImageName(dockerImage); + final NativeAdvancedImageFromDockerfile builder = + new NativeAdvancedImageFromDockerfile(dockerImage, this.config.deleteOnExit()); + + this.configureAbstract(builder, sanitizeDockerImageName); + + this.templateCacheConfigValue(this.config.cacheFrom(), sanitizeDockerImageName) + .ifPresent(builder::withCacheFrom); + + if(!this.config.saveCacheInBackground()) + { + this.configureCacheTo(sanitizeDockerImageName, builder); + } + + final NativeAdvancedImageFromDockerfile customizedBuilder = configure.apply(builder); + + final String builtImageName = this.buildImage(customizedBuilder, timeout); + + if(this.config.saveCacheInBackground() && this.config.cacheTo().isPresent()) + { + LOG.info("Rebuilding image {} in background to save cache", builtImageName); + TCICacheSaveRegistry.instance().add( + CompletableFuture.runAsync( + () -> { + try + { + this.buildImage( + this.configureCacheTo( + sanitizeDockerImageName, + customizedBuilder.copyForExactRebuild(dockerImage + "-cache") + // Don't load it into docker because it's not needed there + .withLoad(false)), + timeout); + } + catch(final Exception ex) + { + LOG.warn("Rebuilding {} to save cache failed", builtImageName, ex); + } + }, + TCIExecutorServiceHolder.instance())); + } + + return builtImageName; + } + + protected NativeAdvancedImageFromDockerfile configureCacheTo( + final String sanitizeDockerImageName, + final NativeAdvancedImageFromDockerfile builder) + { + this.templateCacheConfigValue(this.config.cacheTo(), sanitizeDockerImageName) + .ifPresent(builder::withCacheTo); + return builder; + } + + private Optional templateCacheConfigValue( + final Optional configValue, + final String sanitizeDockerImageName) + { + return configValue + .map(s -> s.replace("$image", sanitizeDockerImageName)) + .map(s -> s.replace("§image", sanitizeDockerImageName)); + } + + @Override + public String image( + final String dockerImage, + final Duration timeout, + final UnaryOperator configure) + { + LOG.info("Starting build of image {}", dockerImage); + + final String sanitizeDockerImageName = this.sanitizeDockerImageName(dockerImage); + final AdvancedImageFromDockerFile builder = + new AdvancedImageFromDockerFile(dockerImage, this.config.deleteOnExit()); + + this.configureAbstract(builder, sanitizeDockerImageName); + + return this.buildImage(configure.apply(builder), timeout); + } + + protected String buildImage( + final AbstractImageFromDockerfile builder, + final Duration timeout) + { + LOG.info("Building image {}...", builder.getDockerImageName()); + final String builtImageName = builder.build(timeout); + LOG.info("Built image {}", builtImageName); + + return builtImageName; + } + + protected > void configureAbstract( + final I builder, final String sanitizeDockerImageName) + { + builder.withLoggerForBuild( + LoggerFactory.getLogger(this.config.loggerForBuildPrefix() + sanitizeDockerImageName)); + } + + protected String sanitizeDockerImageName(final String dockerImage) + { + return DOCKER_IMAGE_SANITIZATION_PATTERN.matcher(dockerImage).replaceAll("_"); + } +} diff --git a/image-build/src/main/java/software/xdev/tci/imagebuild/DefaultCreateImageHandler.java b/image-build/src/main/java/software/xdev/tci/imagebuild/DefaultCreateImageHandler.java deleted file mode 100644 index efc1814d..00000000 --- a/image-build/src/main/java/software/xdev/tci/imagebuild/DefaultCreateImageHandler.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright © 2025 XDEV Software (https://xdev.software) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package software.xdev.tci.imagebuild; - -import java.util.regex.Pattern; - -import org.slf4j.LoggerFactory; - -import software.xdev.tci.imagebuild.config.CreateImageHandlerConfig; -import software.xdev.testcontainers.imagebuilder.AbstractImageFromDockerfile; -import software.xdev.testcontainers.imagebuilder.AdvancedImageFromDockerFile; -import software.xdev.testcontainers.imagebuilder.buildxnative.NativeAdvancedImageFromDockerfile; - - -public class DefaultCreateImageHandler implements CreateImageHandler -{ - protected static final Pattern DOCKER_IMAGE_SANITIZATION_PATTERN = - Pattern.compile("[^A-Za-z0-9-_]"); - - protected final CreateImageHandlerConfig config; - - public DefaultCreateImageHandler() - { - this(CreateImageHandlerConfig.instance()); - } - - public DefaultCreateImageHandler(final CreateImageHandlerConfig config) - { - this.config = config; - } - - @Override - public NativeAdvancedImageFromDockerfile nativeImage(final String dockerImage) - { - final String sanitizeDockerImageName = this.sanitizeDockerImageName(dockerImage); - final NativeAdvancedImageFromDockerfile builder = - new NativeAdvancedImageFromDockerfile(dockerImage, this.config.deleteOnExit()); - - this.configureAbstract(builder, sanitizeDockerImageName); - - this.config.cacheFrom() - .map(s -> s.replace("$image", sanitizeDockerImageName)) - .map(s -> s.replace("§image", sanitizeDockerImageName)) - .ifPresent(builder::withCacheFrom); - this.config.cacheTo() - .map(s -> s.replace("$image", sanitizeDockerImageName)) - .map(s -> s.replace("§image", sanitizeDockerImageName)) - .ifPresent(builder::withCacheTo); - - return builder; - } - - @Override - public AdvancedImageFromDockerFile image(final String dockerImage) - { - final String sanitizeDockerImageName = this.sanitizeDockerImageName(dockerImage); - final AdvancedImageFromDockerFile builder = - new AdvancedImageFromDockerFile(dockerImage, this.config.deleteOnExit()); - - return this.configureAbstract(builder, sanitizeDockerImageName); - } - - protected > I configureAbstract( - final I builder, final String sanitizeDockerImageName) - { - return builder.withLoggerForBuild( - LoggerFactory.getLogger(this.config.loggerForBuildPrefix() + sanitizeDockerImageName)); - } - - protected String sanitizeDockerImageName(final String dockerImage) - { - return DOCKER_IMAGE_SANITIZATION_PATTERN.matcher(dockerImage).replaceAll("_"); - } -} diff --git a/image-build/src/main/java/software/xdev/tci/imagebuild/cache/async/TCICacheSaveRegistry.java b/image-build/src/main/java/software/xdev/tci/imagebuild/cache/async/TCICacheSaveRegistry.java new file mode 100644 index 00000000..fd5868c8 --- /dev/null +++ b/image-build/src/main/java/software/xdev/tci/imagebuild/cache/async/TCICacheSaveRegistry.java @@ -0,0 +1,50 @@ +package software.xdev.tci.imagebuild.cache.async; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; + + +public class TCICacheSaveRegistry +{ + private static TCICacheSaveRegistry instance; + + public static void setInstance(final TCICacheSaveRegistry instance) + { + TCICacheSaveRegistry.instance = instance; + } + + public static TCICacheSaveRegistry instance() + { + if(instance == null) + { + createInstance(); + } + return instance; + } + + private static synchronized void createInstance() + { + if(instance == null) + { + instance = new TCICacheSaveRegistry(); + } + } + + protected final List> cfs = Collections.synchronizedList(new ArrayList<>()); + + public void add(final CompletableFuture cf) + { + this.cfs.add(cf); + } + + public List> get() + { + return this.cfs; + } + + protected TCICacheSaveRegistry() + { + } +} diff --git a/image-build/src/main/java/software/xdev/tci/imagebuild/cache/async/TCIWaitForCacheSave.java b/image-build/src/main/java/software/xdev/tci/imagebuild/cache/async/TCIWaitForCacheSave.java new file mode 100644 index 00000000..02dc49d7 --- /dev/null +++ b/image-build/src/main/java/software/xdev/tci/imagebuild/cache/async/TCIWaitForCacheSave.java @@ -0,0 +1,69 @@ +package software.xdev.tci.imagebuild.cache.async; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.junit.platform.launcher.TestExecutionListener; +import org.junit.platform.launcher.TestPlan; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import software.xdev.tci.imagebuild.config.BuildImageHandlerConfig; + + +public class TCIWaitForCacheSave implements TestExecutionListener +{ + private static final Logger LOG = LoggerFactory.getLogger(TCIWaitForCacheSave.class); + + private BuildImageHandlerConfig config; + + @Override + public void testPlanExecutionStarted(final TestPlan testPlan) + { + this.config = BuildImageHandlerConfig.instance(); + } + + @Override + public void testPlanExecutionFinished(final TestPlan testPlan) + { + if(!this.config.waitForSaveCacheInBackground()) + { + return; + } + + final List> cfs = TCICacheSaveRegistry.instance().get(); + if(cfs.isEmpty()) + { + return; + } + + LOG.info("Waiting for cache saves to finish..."); + final long startMs = System.currentTimeMillis(); + try + { + CompletableFuture.allOf(cfs.toArray(CompletableFuture[]::new)) + .get(10, TimeUnit.MINUTES); + } + catch(final InterruptedException e) + { + LOG.warn("Got interrupted", e); + Thread.currentThread().interrupt(); + } + catch(final ExecutionException e) + { + LOG.warn("A cache save failed", e); + } + catch(final TimeoutException e) + { + LOG.warn("Timed out while waiting for cache save", e); + } + + LOG.info( + "Finished waiting for {}x cache saves, took {}ms", + cfs.size(), + System.currentTimeMillis() - startMs); + } +} diff --git a/image-build/src/main/java/software/xdev/tci/imagebuild/config/CreateImageHandlerConfig.java b/image-build/src/main/java/software/xdev/tci/imagebuild/config/BuildImageHandlerConfig.java similarity index 78% rename from image-build/src/main/java/software/xdev/tci/imagebuild/config/CreateImageHandlerConfig.java rename to image-build/src/main/java/software/xdev/tci/imagebuild/config/BuildImageHandlerConfig.java index 80ac8081..1a16d4d4 100644 --- a/image-build/src/main/java/software/xdev/tci/imagebuild/config/CreateImageHandlerConfig.java +++ b/image-build/src/main/java/software/xdev/tci/imagebuild/config/BuildImageHandlerConfig.java @@ -20,7 +20,7 @@ import software.xdev.tci.serviceloading.TCIServiceLoaderHolder; -public interface CreateImageHandlerConfig +public interface BuildImageHandlerConfig { boolean deleteOnExit(); @@ -30,8 +30,12 @@ public interface CreateImageHandlerConfig Optional cacheTo(); - static CreateImageHandlerConfig instance() + boolean saveCacheInBackground(); + + boolean waitForSaveCacheInBackground(); + + static BuildImageHandlerConfig instance() { - return TCIServiceLoaderHolder.instance().service(CreateImageHandlerConfig.class); + return TCIServiceLoaderHolder.instance().service(BuildImageHandlerConfig.class); } } diff --git a/image-build/src/main/java/software/xdev/tci/imagebuild/config/DefaultCreateImageHandlerConfig.java b/image-build/src/main/java/software/xdev/tci/imagebuild/config/DefaultBuildImageHandlerConfig.java similarity index 69% rename from image-build/src/main/java/software/xdev/tci/imagebuild/config/DefaultCreateImageHandlerConfig.java rename to image-build/src/main/java/software/xdev/tci/imagebuild/config/DefaultBuildImageHandlerConfig.java index 80d09f16..5d43b7ea 100644 --- a/image-build/src/main/java/software/xdev/tci/imagebuild/config/DefaultCreateImageHandlerConfig.java +++ b/image-build/src/main/java/software/xdev/tci/imagebuild/config/DefaultBuildImageHandlerConfig.java @@ -21,12 +21,16 @@ @SuppressWarnings({"java:S2789", "OptionalAssignedToNull", "OptionalUsedAsFieldOrParameterType"}) -public class DefaultCreateImageHandlerConfig extends DefaultConfig implements CreateImageHandlerConfig +public class DefaultBuildImageHandlerConfig extends DefaultConfig implements BuildImageHandlerConfig { + protected static final String SAVE_CACHE_IN_BACKGROUND = "save-cache-in-background"; + protected Boolean deleteOnExit; protected String loggerForBuildPrefix; protected Optional cacheFrom; protected Optional cacheTo; + protected Boolean saveCacheInBackground; + protected Boolean waitForSaveCacheInBackground; @Override protected String propertyNamePrefix() @@ -74,4 +78,26 @@ public Optional cacheTo() } return this.cacheTo; } + + @Override + public boolean saveCacheInBackground() + { + if(this.saveCacheInBackground == null) + { + this.saveCacheInBackground = + this.resolveBool(SAVE_CACHE_IN_BACKGROUND, this.cacheTo().isPresent()); + } + return this.saveCacheInBackground; + } + + @Override + public boolean waitForSaveCacheInBackground() + { + if(this.waitForSaveCacheInBackground == null) + { + this.waitForSaveCacheInBackground = + this.resolveBool("wait-for-" + SAVE_CACHE_IN_BACKGROUND, true); + } + return this.waitForSaveCacheInBackground; + } } diff --git a/image-build/src/main/resources/META-INF/services/org.junit.platform.launcher.TestExecutionListener b/image-build/src/main/resources/META-INF/services/org.junit.platform.launcher.TestExecutionListener new file mode 100644 index 00000000..8604e1ab --- /dev/null +++ b/image-build/src/main/resources/META-INF/services/org.junit.platform.launcher.TestExecutionListener @@ -0,0 +1 @@ +software.xdev.tci.imagebuild.cache.async.TCIWaitForCacheSave diff --git a/image-build/src/main/resources/META-INF/services/software.xdev.tci.imagebuild.BuildImageHandler b/image-build/src/main/resources/META-INF/services/software.xdev.tci.imagebuild.BuildImageHandler new file mode 100644 index 00000000..0741e473 --- /dev/null +++ b/image-build/src/main/resources/META-INF/services/software.xdev.tci.imagebuild.BuildImageHandler @@ -0,0 +1 @@ +software.xdev.tci.imagebuild.DefaultBuildImageHandler diff --git a/image-build/src/main/resources/META-INF/services/software.xdev.tci.imagebuild.CreateImageHandler b/image-build/src/main/resources/META-INF/services/software.xdev.tci.imagebuild.CreateImageHandler deleted file mode 100644 index f76ede0c..00000000 --- a/image-build/src/main/resources/META-INF/services/software.xdev.tci.imagebuild.CreateImageHandler +++ /dev/null @@ -1 +0,0 @@ -software.xdev.tci.imagebuild.DefaultCreateImageHandler diff --git a/image-build/src/main/resources/META-INF/services/software.xdev.tci.imagebuild.config.BuildImageHandlerConfig b/image-build/src/main/resources/META-INF/services/software.xdev.tci.imagebuild.config.BuildImageHandlerConfig new file mode 100644 index 00000000..b71d6a3e --- /dev/null +++ b/image-build/src/main/resources/META-INF/services/software.xdev.tci.imagebuild.config.BuildImageHandlerConfig @@ -0,0 +1 @@ +software.xdev.tci.imagebuild.config.DefaultBuildImageHandlerConfig diff --git a/image-build/src/main/resources/META-INF/services/software.xdev.tci.imagebuild.config.CreateImageHandlerConfig b/image-build/src/main/resources/META-INF/services/software.xdev.tci.imagebuild.config.CreateImageHandlerConfig deleted file mode 100644 index 9360b4bb..00000000 --- a/image-build/src/main/resources/META-INF/services/software.xdev.tci.imagebuild.config.CreateImageHandlerConfig +++ /dev/null @@ -1 +0,0 @@ -software.xdev.tci.imagebuild.config.DefaultCreateImageHandlerConfig From 4f30dff434175ae8d9f1d85dc9380145d070e24f Mon Sep 17 00:00:00 2001 From: AB Date: Fri, 10 Jul 2026 12:11:23 +0200 Subject: [PATCH 097/141] Add misisng headers --- .../cache/async/TCICacheSaveRegistry.java | 15 +++++++++++++++ .../cache/async/TCIWaitForCacheSave.java | 15 +++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/image-build/src/main/java/software/xdev/tci/imagebuild/cache/async/TCICacheSaveRegistry.java b/image-build/src/main/java/software/xdev/tci/imagebuild/cache/async/TCICacheSaveRegistry.java index fd5868c8..e4e8ebd3 100644 --- a/image-build/src/main/java/software/xdev/tci/imagebuild/cache/async/TCICacheSaveRegistry.java +++ b/image-build/src/main/java/software/xdev/tci/imagebuild/cache/async/TCICacheSaveRegistry.java @@ -1,3 +1,18 @@ +/* + * Copyright © 2025 XDEV Software (https://xdev.software) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package software.xdev.tci.imagebuild.cache.async; import java.util.ArrayList; diff --git a/image-build/src/main/java/software/xdev/tci/imagebuild/cache/async/TCIWaitForCacheSave.java b/image-build/src/main/java/software/xdev/tci/imagebuild/cache/async/TCIWaitForCacheSave.java index 02dc49d7..eb81e7d8 100644 --- a/image-build/src/main/java/software/xdev/tci/imagebuild/cache/async/TCIWaitForCacheSave.java +++ b/image-build/src/main/java/software/xdev/tci/imagebuild/cache/async/TCIWaitForCacheSave.java @@ -1,3 +1,18 @@ +/* + * Copyright © 2025 XDEV Software (https://xdev.software) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package software.xdev.tci.imagebuild.cache.async; import java.util.List; From 42563af385da9872c9ac7a9bcc321ad2613bac41 Mon Sep 17 00:00:00 2001 From: AB Date: Fri, 10 Jul 2026 12:17:48 +0200 Subject: [PATCH 098/141] Unsnap version --- image-build/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/image-build/pom.xml b/image-build/pom.xml index ba656ab9..61dcbd64 100644 --- a/image-build/pom.xml +++ b/image-build/pom.xml @@ -58,7 +58,7 @@ software.xdev testcontainers-advanced-imagebuilder - 4.1.0-SNAPSHOT + 4.1.0 From 35c83930c4840e9ba5ad465ef0bc7d0a40e2e750 Mon Sep 17 00:00:00 2001 From: AB Date: Fri, 10 Jul 2026 13:03:06 +0200 Subject: [PATCH 099/141] Correctly instantiate transfer cache --- image-build/pom.xml | 2 +- .../imagebuild/DefaultBuildImageHandler.java | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/image-build/pom.xml b/image-build/pom.xml index 61dcbd64..dc1d768d 100644 --- a/image-build/pom.xml +++ b/image-build/pom.xml @@ -58,7 +58,7 @@ software.xdev testcontainers-advanced-imagebuilder - 4.1.0 + 4.1.1 diff --git a/image-build/src/main/java/software/xdev/tci/imagebuild/DefaultBuildImageHandler.java b/image-build/src/main/java/software/xdev/tci/imagebuild/DefaultBuildImageHandler.java index 589ab297..0a1360af 100644 --- a/image-build/src/main/java/software/xdev/tci/imagebuild/DefaultBuildImageHandler.java +++ b/image-build/src/main/java/software/xdev/tci/imagebuild/DefaultBuildImageHandler.java @@ -68,7 +68,11 @@ public String nativeImage( this.templateCacheConfigValue(this.config.cacheFrom(), sanitizeDockerImageName) .ifPresent(builder::withCacheFrom); - if(!this.config.saveCacheInBackground()) + if(this.config.saveCacheInBackground()) + { + builder.withCreateTransferFilesCache(true); + } + else { this.configureCacheTo(sanitizeDockerImageName, builder); } @@ -97,6 +101,17 @@ public String nativeImage( { LOG.warn("Rebuilding {} to save cache failed", builtImageName, ex); } + finally + { + try + { + builder.cleanCreatedTransferFilesCache(); + } + catch(final Exception ex) + { + LOG.warn("Failed to clean created transfer file cache for {}", builtImageName, ex); + } + } }, TCIExecutorServiceHolder.instance())); } From b692bc7170def1a0f8f59ef6321762a56f42744e Mon Sep 17 00:00:00 2001 From: AB Date: Fri, 10 Jul 2026 14:38:02 +0200 Subject: [PATCH 100/141] Bump version --- image-build/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/image-build/pom.xml b/image-build/pom.xml index dc1d768d..7f51e203 100644 --- a/image-build/pom.xml +++ b/image-build/pom.xml @@ -58,7 +58,7 @@ software.xdev testcontainers-advanced-imagebuilder - 4.1.1 + 4.1.2 From d5d7f92b24db48c4ae8cdf68048876a480159b4a Mon Sep 17 00:00:00 2001 From: XDEV Renovate Bot Date: Sun, 12 Jul 2026 06:26:11 +0000 Subject: [PATCH 101/141] Update dependency com.puppycrawl.tools:checkstyle to v13.8.0 --- pom.xml | 2 +- template-placeholder/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 6eb41f92..9642b3fc 100644 --- a/pom.xml +++ b/pom.xml @@ -45,7 +45,7 @@ com.puppycrawl.tools checkstyle - 13.7.0 + 13.8.0 diff --git a/template-placeholder/pom.xml b/template-placeholder/pom.xml index 9a861f91..c1cb5518 100644 --- a/template-placeholder/pom.xml +++ b/template-placeholder/pom.xml @@ -215,7 +215,7 @@ com.puppycrawl.tools checkstyle - 13.7.0 + 13.8.0 From 760dc76727f144db900853b8f4321e836ccb8667 Mon Sep 17 00:00:00 2001 From: XDEV Renovate Bot Date: Sun, 12 Jul 2026 06:26:27 +0000 Subject: [PATCH 102/141] Update org.hibernate.orm to v7.4.5.Final --- db-jdbc-spring-orm-hibernate/pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/db-jdbc-spring-orm-hibernate/pom.xml b/db-jdbc-spring-orm-hibernate/pom.xml index fdc1bca8..943cca58 100644 --- a/db-jdbc-spring-orm-hibernate/pom.xml +++ b/db-jdbc-spring-orm-hibernate/pom.xml @@ -59,18 +59,18 @@ org.hibernate.orm hibernate-core - 7.4.4.Final + 7.4.5.Final org.hibernate.orm hibernate-scan-jandex - 7.4.4.Final + 7.4.5.Final org.hibernate.orm hibernate-hikaricp - 7.4.4.Final + 7.4.5.Final From b2e0f35a14631ef65c4c8de69ed0a6e57ea69610 Mon Sep 17 00:00:00 2001 From: XDEV Renovate Bot Date: Mon, 13 Jul 2026 06:46:59 +0000 Subject: [PATCH 103/141] Update org.junit to v6.1.2 --- advanced-demo/integration-tests/pom.xml | 2 +- advanced-demo/pom.xml | 2 +- base-demo/pom.xml | 2 +- base/pom.xml | 4 ++-- db-jdbc/pom.xml | 2 +- junit-jupiter-api-support/pom.xml | 2 +- spring-dao-support/pom.xml | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/advanced-demo/integration-tests/pom.xml b/advanced-demo/integration-tests/pom.xml index 07788024..2d54e5b4 100644 --- a/advanced-demo/integration-tests/pom.xml +++ b/advanced-demo/integration-tests/pom.xml @@ -114,7 +114,7 @@ org.junit junit-bom - 6.1.1 + 6.1.2 pom import diff --git a/advanced-demo/pom.xml b/advanced-demo/pom.xml index 7b2e3021..a132c67f 100644 --- a/advanced-demo/pom.xml +++ b/advanced-demo/pom.xml @@ -70,7 +70,7 @@ org.junit junit-bom - 6.1.1 + 6.1.2 pom import diff --git a/base-demo/pom.xml b/base-demo/pom.xml index acaf7037..ec51c51e 100644 --- a/base-demo/pom.xml +++ b/base-demo/pom.xml @@ -38,7 +38,7 @@ org.junit.jupiter junit-jupiter - 6.1.1 + 6.1.2 test diff --git a/base/pom.xml b/base/pom.xml index 42c60a2d..819ad072 100644 --- a/base/pom.xml +++ b/base/pom.xml @@ -69,14 +69,14 @@ org.junit.platform junit-platform-launcher compile - 6.1.1 + 6.1.2
org.junit.jupiter junit-jupiter - 6.1.1 + 6.1.2 test diff --git a/db-jdbc/pom.xml b/db-jdbc/pom.xml index c1fbbc64..a17b1ff5 100644 --- a/db-jdbc/pom.xml +++ b/db-jdbc/pom.xml @@ -72,7 +72,7 @@ org.junit.jupiter junit-jupiter - 6.1.1 + 6.1.2 test diff --git a/junit-jupiter-api-support/pom.xml b/junit-jupiter-api-support/pom.xml index 12fb28f2..8a07bbaa 100644 --- a/junit-jupiter-api-support/pom.xml +++ b/junit-jupiter-api-support/pom.xml @@ -53,7 +53,7 @@ org.junit.jupiter junit-jupiter-api - 6.1.1 + 6.1.2 compile diff --git a/spring-dao-support/pom.xml b/spring-dao-support/pom.xml index 25d18c4a..583eb77d 100644 --- a/spring-dao-support/pom.xml +++ b/spring-dao-support/pom.xml @@ -78,7 +78,7 @@ org.junit.jupiter junit-jupiter - 6.1.1 + 6.1.2 test From 504e727da1159e928f6b1f45210ded2430188ead Mon Sep 17 00:00:00 2001 From: XDEV Renovate Bot Date: Mon, 13 Jul 2026 06:47:03 +0000 Subject: [PATCH 104/141] Update dependency com.puppycrawl.tools:checkstyle to v13.8.0 --- advanced-demo/pom.xml | 2 +- base/pom.xml | 2 +- db-jdbc-spring-orm-eclipselink/pom.xml | 2 +- db-jdbc-spring-orm-hibernate/pom.xml | 2 +- db-jdbc-spring-orm/pom.xml | 2 +- db-jdbc/pom.xml | 2 +- image-build/pom.xml | 2 +- jacoco/pom.xml | 2 +- jul-to-slf4j/pom.xml | 2 +- junit-jupiter-api-support/pom.xml | 2 +- mockserver/pom.xml | 2 +- oidc-server-mock/pom.xml | 2 +- pom.xml | 2 +- selenium/pom.xml | 2 +- spring-dao-support/pom.xml | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) diff --git a/advanced-demo/pom.xml b/advanced-demo/pom.xml index 7b2e3021..e630bca9 100644 --- a/advanced-demo/pom.xml +++ b/advanced-demo/pom.xml @@ -195,7 +195,7 @@ com.puppycrawl.tools checkstyle - 13.7.0 + 13.8.0 diff --git a/base/pom.xml b/base/pom.xml index 42c60a2d..31a93f5e 100644 --- a/base/pom.xml +++ b/base/pom.xml @@ -284,7 +284,7 @@ com.puppycrawl.tools checkstyle - 13.7.0 + 13.8.0 diff --git a/db-jdbc-spring-orm-eclipselink/pom.xml b/db-jdbc-spring-orm-eclipselink/pom.xml index a7689a23..16226b22 100644 --- a/db-jdbc-spring-orm-eclipselink/pom.xml +++ b/db-jdbc-spring-orm-eclipselink/pom.xml @@ -248,7 +248,7 @@ com.puppycrawl.tools checkstyle - 13.7.0 + 13.8.0 diff --git a/db-jdbc-spring-orm-hibernate/pom.xml b/db-jdbc-spring-orm-hibernate/pom.xml index 943cca58..9e11668f 100644 --- a/db-jdbc-spring-orm-hibernate/pom.xml +++ b/db-jdbc-spring-orm-hibernate/pom.xml @@ -259,7 +259,7 @@ com.puppycrawl.tools checkstyle - 13.7.0 + 13.8.0 diff --git a/db-jdbc-spring-orm/pom.xml b/db-jdbc-spring-orm/pom.xml index c19b8c88..fc5c488f 100644 --- a/db-jdbc-spring-orm/pom.xml +++ b/db-jdbc-spring-orm/pom.xml @@ -258,7 +258,7 @@ com.puppycrawl.tools checkstyle - 13.7.0 + 13.8.0 diff --git a/db-jdbc/pom.xml b/db-jdbc/pom.xml index c1fbbc64..8597ca05 100644 --- a/db-jdbc/pom.xml +++ b/db-jdbc/pom.xml @@ -268,7 +268,7 @@ com.puppycrawl.tools checkstyle - 13.7.0 + 13.8.0 diff --git a/image-build/pom.xml b/image-build/pom.xml index 7f51e203..0efd17d3 100644 --- a/image-build/pom.xml +++ b/image-build/pom.xml @@ -247,7 +247,7 @@ com.puppycrawl.tools checkstyle - 13.7.0 + 13.8.0 diff --git a/jacoco/pom.xml b/jacoco/pom.xml index 14a24c07..253a09de 100644 --- a/jacoco/pom.xml +++ b/jacoco/pom.xml @@ -242,7 +242,7 @@ com.puppycrawl.tools checkstyle - 13.7.0 + 13.8.0 diff --git a/jul-to-slf4j/pom.xml b/jul-to-slf4j/pom.xml index c7018e70..f60c13fe 100644 --- a/jul-to-slf4j/pom.xml +++ b/jul-to-slf4j/pom.xml @@ -242,7 +242,7 @@ com.puppycrawl.tools checkstyle - 13.7.0 + 13.8.0 diff --git a/junit-jupiter-api-support/pom.xml b/junit-jupiter-api-support/pom.xml index 12fb28f2..7397051d 100644 --- a/junit-jupiter-api-support/pom.xml +++ b/junit-jupiter-api-support/pom.xml @@ -243,7 +243,7 @@ com.puppycrawl.tools checkstyle - 13.7.0 + 13.8.0 diff --git a/mockserver/pom.xml b/mockserver/pom.xml index f5740aa8..2eec4022 100644 --- a/mockserver/pom.xml +++ b/mockserver/pom.xml @@ -264,7 +264,7 @@ com.puppycrawl.tools checkstyle - 13.7.0 + 13.8.0 diff --git a/oidc-server-mock/pom.xml b/oidc-server-mock/pom.xml index b3dd16a4..fad26177 100644 --- a/oidc-server-mock/pom.xml +++ b/oidc-server-mock/pom.xml @@ -248,7 +248,7 @@ com.puppycrawl.tools checkstyle - 13.7.0 + 13.8.0 diff --git a/pom.xml b/pom.xml index 496b07d5..fdc2b45a 100644 --- a/pom.xml +++ b/pom.xml @@ -78,7 +78,7 @@ com.puppycrawl.tools checkstyle - 13.7.0 + 13.8.0 diff --git a/selenium/pom.xml b/selenium/pom.xml index e94800ab..1d58c858 100644 --- a/selenium/pom.xml +++ b/selenium/pom.xml @@ -296,7 +296,7 @@ com.puppycrawl.tools checkstyle - 13.7.0 + 13.8.0 diff --git a/spring-dao-support/pom.xml b/spring-dao-support/pom.xml index 25d18c4a..d7995373 100644 --- a/spring-dao-support/pom.xml +++ b/spring-dao-support/pom.xml @@ -274,7 +274,7 @@ com.puppycrawl.tools checkstyle - 13.7.0 + 13.8.0 From f8c1cdd1d3761cd7fb8beb77872232704798f1ad Mon Sep 17 00:00:00 2001 From: AB Date: Mon, 13 Jul 2026 17:22:34 +0200 Subject: [PATCH 105/141] Add HsErrPidCrashReporter See #688 --- .../webapp/containers/WebAppContainer.java | 19 ++- .../fatal/HsErrPidStartUpCrashReporter.java | 121 ++++++++++++++++++ .../cache/async/TCICacheSaveRegistry.java | 11 +- .../tci/jacoco/testbase/JaCoCoRecorder.java | 10 +- 4 files changed, 151 insertions(+), 10 deletions(-) create mode 100644 base/src/main/java/software/xdev/tci/startup/error/java/fatal/HsErrPidStartUpCrashReporter.java diff --git a/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/containers/WebAppContainer.java b/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/containers/WebAppContainer.java index 56a6e63c..36aa26db 100644 --- a/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/containers/WebAppContainer.java +++ b/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/containers/WebAppContainer.java @@ -6,9 +6,11 @@ import org.testcontainers.containers.wait.strategy.HttpWaitStrategy; import org.testcontainers.containers.wait.strategy.WaitAllStrategy; +import com.github.dockerjava.api.command.InspectContainerResponse; import com.github.dockerjava.zerodep.shaded.org.apache.hc.core5.http.HttpStatus; import software.xdev.tci.jacoco.containers.JaCoCoAwareContainer; +import software.xdev.tci.startup.error.java.fatal.HsErrPidStartUpCrashReporter; @SuppressWarnings("java:S2160") @@ -17,6 +19,7 @@ public class WebAppContainer extends GenericContainer implement public static final int DEFAULT_HTTP_PORT = 8080; protected final boolean connectionlessStart; + protected final HsErrPidStartUpCrashReporter hsErrPidStartUpCrashReporter; public WebAppContainer(final String dockerImageName, final boolean connectionlessStart) { @@ -27,6 +30,7 @@ public WebAppContainer(final String dockerImageName, final boolean connectionles this.withConnectionlessStart(); } this.addExposedPort(DEFAULT_HTTP_PORT); + this.hsErrPidStartUpCrashReporter = new HsErrPidStartUpCrashReporter(this); } public WebAppContainer withDB(final String jdbcUrl, final String username, final String password) @@ -88,7 +92,7 @@ public WebAppContainer withConnectionlessStart() .withEnv("MANAGEMENT_HEALTH_DB_ENABLED", false) .withEnv(springJpa + "PROPERTIES_JAKARTA_PERSISTENCE_DATABASE-PRODUCT-NAME", "MariaDB") .withEnv(springJpa + "PROPERTIES_JAKARTA_PERSISTENCE_DATABASE-MAJOR-VERSION", "11") - .withEnv(springJpa + "PROPERTIES_JAKARTA_PERSISTENCE_DATABASE-MINOR-VERSION", "4") + .withEnv(springJpa + "PROPERTIES_JAKARTA_PERSISTENCE_DATABASE-MINOR-VERSION", "8") .withEnv(springJpa + "PROPERTIES_HIBERNATE_BOOT_ALLOW_JDBC_METADATA_ACCESS", false); } @@ -124,6 +128,19 @@ public WebAppContainer withDefaultWaitStrategy( .forStatusCode(HttpStatus.SC_OK) .withReadTimeout(Duration.ofSeconds(10)) )); + + @Override + protected void containerIsStarted(final InspectContainerResponse containerInfo, final boolean reused) + { + this.hsErrPidStartUpCrashReporter.containerIsStarted(); + super.containerIsStarted(containerInfo, reused); + } + + @Override + protected void containerIsStopping(final InspectContainerResponse containerInfo) + { + this.hsErrPidStartUpCrashReporter.containerIsStopping(this.logger()); + super.containerIsStopping(containerInfo); } @SuppressWarnings("unused") diff --git a/base/src/main/java/software/xdev/tci/startup/error/java/fatal/HsErrPidStartUpCrashReporter.java b/base/src/main/java/software/xdev/tci/startup/error/java/fatal/HsErrPidStartUpCrashReporter.java new file mode 100644 index 00000000..d804a7bf --- /dev/null +++ b/base/src/main/java/software/xdev/tci/startup/error/java/fatal/HsErrPidStartUpCrashReporter.java @@ -0,0 +1,121 @@ +package software.xdev.tci.startup.error.java.fatal; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.function.Supplier; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; +import org.testcontainers.containers.GenericContainer; + +import software.xdev.tci.concurrent.Suppliers; + + +/** + * Utility to log contents of java hs_err_pid files of a container that failed to start. + *

+ * Additionally also copies the file into target/ + *

+ */ +public class HsErrPidStartUpCrashReporter +{ + protected static final String HS_ERR_PID = "hs_err_pid"; + protected static final Supplier DEFAULT_HS_ERR_PID_EXTRACTION_PATTERN_SUPPLIER = + Suppliers.memoize(() -> Pattern.compile("^# (\\S{1,999}hs_err_pid\\d{1,9}\\.log)$", Pattern.MULTILINE)); + + protected final GenericContainer container; + protected final String hsErrPidSearchKey; + protected final Supplier hsErrPidExtractionPattern; + protected Supplier hsErrPidPathSupplier = Suppliers.memoize( + () -> Paths.get("target/has_err_pid")); + + protected boolean startedWithoutCrashing; + + public HsErrPidStartUpCrashReporter(final GenericContainer container) + { + this(container, HS_ERR_PID, DEFAULT_HS_ERR_PID_EXTRACTION_PATTERN_SUPPLIER); + } + + protected HsErrPidStartUpCrashReporter( + final GenericContainer container, + final String hsErrPidSearchKey, + final Supplier hsErrPidExtractionPattern) + { + this.container = container; + this.hsErrPidSearchKey = hsErrPidSearchKey; + this.hsErrPidExtractionPattern = hsErrPidExtractionPattern; + } + + public void containerIsStarted() + { + this.startedWithoutCrashing = true; + } + + public void containerIsStopping(final Logger logger) + { + if(this.startedWithoutCrashing || !logger.isWarnEnabled()) + { + return; + } + + final String logs = this.container.getLogs(); + if(!logs.contains(this.hsErrPidSearchKey)) + { + return; + } + + final Matcher matcher = this.hsErrPidExtractionPattern.get().matcher(logs); + if(!matcher.find()) + { + logger.warn( + "Detected {} in log output but was unable to determine the file location - " + + "No regex match", this.hsErrPidSearchKey); + return; + } + + final String filePath = matcher.group(1); + if(filePath == null || filePath.isBlank()) + { + logger.warn( + "Detected {} in log output but was unable to determine the file location - " + + "No regex group match", this.hsErrPidSearchKey); + return; + } + + try + { + final String containerId = this.container.getContainerId(); + + final Path hsErrPidContainerDirPath = this.hsErrPidPathSupplier.get().resolve(containerId); + Files.createDirectories(hsErrPidContainerDirPath); + final Path targetHsErrPidFilePath = + hsErrPidContainerDirPath.resolve(Path.of(filePath).getFileName().toString()); + + this.container.copyFileFromContainer( + filePath, + is -> Files.copy( + is, + targetHsErrPidFilePath, + StandardCopyOption.REPLACE_EXISTING)); + + logger.warn( + "Detected {} file[container={}]: {} -> {} \n{}", + this.hsErrPidSearchKey, + containerId, + filePath, + targetHsErrPidFilePath, + this.container.copyFileFromContainer( + filePath, + is -> IOUtils.toString(is, StandardCharsets.UTF_8))); + } + catch(final Exception ex) + { + logger.warn("Failed to read {} file {}", this.hsErrPidSearchKey, filePath, ex); + } + } +} diff --git a/image-build/src/main/java/software/xdev/tci/imagebuild/cache/async/TCICacheSaveRegistry.java b/image-build/src/main/java/software/xdev/tci/imagebuild/cache/async/TCICacheSaveRegistry.java index e4e8ebd3..aaae1d0f 100644 --- a/image-build/src/main/java/software/xdev/tci/imagebuild/cache/async/TCICacheSaveRegistry.java +++ b/image-build/src/main/java/software/xdev/tci/imagebuild/cache/async/TCICacheSaveRegistry.java @@ -23,23 +23,18 @@ public class TCICacheSaveRegistry { - private static TCICacheSaveRegistry instance; - - public static void setInstance(final TCICacheSaveRegistry instance) - { - TCICacheSaveRegistry.instance = instance; - } + protected static TCICacheSaveRegistry instance; public static TCICacheSaveRegistry instance() { if(instance == null) { - createInstance(); + createDefaultInstance(); } return instance; } - private static synchronized void createInstance() + protected static synchronized void createDefaultInstance() { if(instance == null) { diff --git a/jacoco/src/main/java/software/xdev/tci/jacoco/testbase/JaCoCoRecorder.java b/jacoco/src/main/java/software/xdev/tci/jacoco/testbase/JaCoCoRecorder.java index 428271eb..a8a2b799 100644 --- a/jacoco/src/main/java/software/xdev/tci/jacoco/testbase/JaCoCoRecorder.java +++ b/jacoco/src/main/java/software/xdev/tci/jacoco/testbase/JaCoCoRecorder.java @@ -46,11 +46,19 @@ public static JaCoCoRecorder instance() { if(instance == null) { - instance = new JaCoCoRecorder(); + createDefaultInstance(); } return instance; } + protected static synchronized void createDefaultInstance() + { + if(instance == null) + { + instance = new JaCoCoRecorder(); + } + } + protected JaCoCoConfig config; protected Map cachedDirForExecutionDataFilesVariantPaths = new ConcurrentHashMap<>(); From 13f438f8317b690483d6abd604f06dae3e8060dc Mon Sep 17 00:00:00 2001 From: AB Date: Mon, 13 Jul 2026 17:24:46 +0200 Subject: [PATCH 106/141] Add missing header --- .../java/fatal/HsErrPidStartUpCrashReporter.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/base/src/main/java/software/xdev/tci/startup/error/java/fatal/HsErrPidStartUpCrashReporter.java b/base/src/main/java/software/xdev/tci/startup/error/java/fatal/HsErrPidStartUpCrashReporter.java index d804a7bf..d48d96e3 100644 --- a/base/src/main/java/software/xdev/tci/startup/error/java/fatal/HsErrPidStartUpCrashReporter.java +++ b/base/src/main/java/software/xdev/tci/startup/error/java/fatal/HsErrPidStartUpCrashReporter.java @@ -1,3 +1,18 @@ +/* + * Copyright © 2024 XDEV Software (https://xdev.software) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package software.xdev.tci.startup.error.java.fatal; import java.nio.charset.StandardCharsets; From 9c481b19ff5378acb8486a729777272a8f0d324c Mon Sep 17 00:00:00 2001 From: AB Date: Mon, 13 Jul 2026 17:26:13 +0200 Subject: [PATCH 107/141] Fix typo --- .../xdev/tci/demo/tci/webapp/containers/WebAppContainer.java | 1 + 1 file changed, 1 insertion(+) diff --git a/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/containers/WebAppContainer.java b/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/containers/WebAppContainer.java index 36aa26db..995a8faa 100644 --- a/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/containers/WebAppContainer.java +++ b/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/containers/WebAppContainer.java @@ -128,6 +128,7 @@ public WebAppContainer withDefaultWaitStrategy( .forStatusCode(HttpStatus.SC_OK) .withReadTimeout(Duration.ofSeconds(10)) )); + } @Override protected void containerIsStarted(final InspectContainerResponse containerInfo, final boolean reused) From a7e85dae62865945c5c533f69a593062584a7f9d Mon Sep 17 00:00:00 2001 From: XDEV Renovate Bot Date: Tue, 14 Jul 2026 05:59:31 +0000 Subject: [PATCH 108/141] Update dependency org.seleniumhq.selenium:selenium-dependencies-bom to v4.46.0 --- advanced-demo/integration-tests/pom.xml | 2 +- selenium/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/advanced-demo/integration-tests/pom.xml b/advanced-demo/integration-tests/pom.xml index 2d54e5b4..f40e331b 100644 --- a/advanced-demo/integration-tests/pom.xml +++ b/advanced-demo/integration-tests/pom.xml @@ -97,7 +97,7 @@ org.seleniumhq.selenium selenium-dependencies-bom - 4.45.0 + 4.46.0 pom import diff --git a/selenium/pom.xml b/selenium/pom.xml index 1d58c858..137ba8ed 100644 --- a/selenium/pom.xml +++ b/selenium/pom.xml @@ -54,7 +54,7 @@ org.seleniumhq.selenium selenium-dependencies-bom - 4.45.0 + 4.46.0 pom import From 9b77c3c4d2df4bc423db35745bacd4bbea6c404c Mon Sep 17 00:00:00 2001 From: AB Date: Tue, 14 Jul 2026 11:15:16 +0200 Subject: [PATCH 109/141] Improve container cleanup --- CHANGELOG.md | 1 + .../xdev/tci/safestart/SafeNamedContainerStarter.java | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd93637a..554eeb0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ * No longer use `setPersistenceProviderClassName` because it has no effect * Performance: Reuse `PersistenceProvider` * Fixed a deadlock during service resolution +* Improve container cleanup * Updated dependencies * Updated demo diff --git a/base/src/main/java/software/xdev/tci/safestart/SafeNamedContainerStarter.java b/base/src/main/java/software/xdev/tci/safestart/SafeNamedContainerStarter.java index 6f800b68..636f5685 100644 --- a/base/src/main/java/software/xdev/tci/safestart/SafeNamedContainerStarter.java +++ b/base/src/main/java/software/xdev/tci/safestart/SafeNamedContainerStarter.java @@ -30,6 +30,8 @@ import org.testcontainers.DockerClientFactory; import org.testcontainers.containers.GenericContainer; +import com.github.dockerjava.api.exception.NotFoundException; + import software.xdev.tci.envperf.EnvironmentPerformance; @@ -144,6 +146,15 @@ protected void tryCleanupContainerAfterStartFail(final List containerNam } catch(final Exception ex) { + // Check if container was already cleaned up (e.g. with tryStart->stop) + if(ex.getCause() instanceof final NotFoundException nfe + && nfe.getMessage() != null + && nfe.getMessage().startsWith("{\"message\":\"No such container:")) + { + LOG.info("Container[name='{}'] was already removed", containerName); + return; + } + LOG.warn("Unable to cleanup container[name='{}']", containerName, ex); } } From 1252f741b10b8216b37b9f34b023f5c778e0f234 Mon Sep 17 00:00:00 2001 From: AB Date: Tue, 14 Jul 2026 11:15:46 +0200 Subject: [PATCH 110/141] Cleanup --- .../tci/jacoco/testbase/JaCoCoRecorder.java | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/jacoco/src/main/java/software/xdev/tci/jacoco/testbase/JaCoCoRecorder.java b/jacoco/src/main/java/software/xdev/tci/jacoco/testbase/JaCoCoRecorder.java index a8a2b799..455e851f 100644 --- a/jacoco/src/main/java/software/xdev/tci/jacoco/testbase/JaCoCoRecorder.java +++ b/jacoco/src/main/java/software/xdev/tci/jacoco/testbase/JaCoCoRecorder.java @@ -87,7 +87,6 @@ public CompletableFuture afterTestAsync( return this.afterTestAsync(optTCI, null, fileSystemFriendlyNameSupplier, variantName); } - @SuppressWarnings("resource") public CompletableFuture afterTestAsync( final Optional> optTCI, final String jaCoCoExecutionDataFilePathInContainer, @@ -132,18 +131,7 @@ protected void stopContainerAndCopy( final GenericContainer container, final String containerPath) { - if(container.isRunning()) - { - // Shutdown container so that jacoco agent dumps the execution data file - try - { - DockerClientFactory.lazyClient().stopContainerCmd(container.getContainerId()).exec(); - } - catch(final Exception ex) - { - LOG.warn("Failed to stop container", ex); - } - } + this.stopContainer(container); try { @@ -168,6 +156,23 @@ protected void stopContainerAndCopy( } } + @SuppressWarnings("resource") + protected void stopContainer(final GenericContainer container) + { + if(container.isRunning()) + { + // Shutdown container so that jacoco agent dumps the execution data file + try + { + DockerClientFactory.lazyClient().stopContainerCmd(container.getContainerId()).exec(); + } + catch(final Exception ex) + { + LOG.warn("Failed to stop container", ex); + } + } + } + protected Path resolveDirForExecutionDataFiles(final String variantName) { return this.cachedDirForExecutionDataFilesVariantPaths.computeIfAbsent( From d3d6387ad871c5041bba8ca9d1c37faa980fdf51 Mon Sep 17 00:00:00 2001 From: AB Date: Tue, 14 Jul 2026 11:58:50 +0200 Subject: [PATCH 111/141] Fix typo --- .../software/xdev/tci/safestart/SafeNamedContainerStarter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/src/main/java/software/xdev/tci/safestart/SafeNamedContainerStarter.java b/base/src/main/java/software/xdev/tci/safestart/SafeNamedContainerStarter.java index 636f5685..36d8d753 100644 --- a/base/src/main/java/software/xdev/tci/safestart/SafeNamedContainerStarter.java +++ b/base/src/main/java/software/xdev/tci/safestart/SafeNamedContainerStarter.java @@ -149,7 +149,7 @@ protected void tryCleanupContainerAfterStartFail(final List containerNam // Check if container was already cleaned up (e.g. with tryStart->stop) if(ex.getCause() instanceof final NotFoundException nfe && nfe.getMessage() != null - && nfe.getMessage().startsWith("{\"message\":\"No such container:")) + && nfe.getMessage().startsWith("Status 404: {\"message\":\"No such container:")) { LOG.info("Container[name='{}'] was already removed", containerName); return; From 0a8d672c44ad2cfda724eb7699d486a0963da205 Mon Sep 17 00:00:00 2001 From: AB Date: Tue, 14 Jul 2026 14:06:57 +0200 Subject: [PATCH 112/141] Added abortable wait strategies --- CHANGELOG.md | 5 + .../webapp/containers/WebAppContainer.java | 20 +- base/pom.xml | 22 + .../xdev/tci/startup/wait/AbortMonitor.java | 45 ++ ...FastAbortOnContainerDeathWaitStrategy.java | 166 ++++++ .../wait/holder/AbortableStrategyValues.java | 28 + .../holder/AbortableStrategyValuesHolder.java | 46 ++ .../AbstractWaitAbortableStrategy.java | 75 +++ .../HostPortWaitAbortableStrategy.java | 172 ++++++ .../strategy/HttpWaitAbortableStrategy.java | 524 ++++++++++++++++++ .../LogMessageWaitAbortableStrategy.java | 101 ++++ .../strategy/WaitAllAbortableStrategy.java | 132 +++++ .../holder/AbortableStrategyValuesHolder.java | 21 + .../db/containers/WaitableJDBCContainer.java | 62 +-- .../containers/TCIMockserverContainer.java | 82 +++ .../factory/MockServerTCIFactory.java | 6 +- .../PreStartableMockServerTCIFactory.java | 6 +- .../tci/oidc/factory/BaseOIDCTCIFactory.java | 29 +- .../selenium/factory/BrowserTCIFactory.java | 17 +- 19 files changed, 1480 insertions(+), 79 deletions(-) create mode 100644 base/src/main/java/software/xdev/tci/startup/wait/AbortMonitor.java create mode 100644 base/src/main/java/software/xdev/tci/startup/wait/FastAbortOnContainerDeathWaitStrategy.java create mode 100644 base/src/main/java/software/xdev/tci/startup/wait/holder/AbortableStrategyValues.java create mode 100644 base/src/main/java/software/xdev/tci/startup/wait/holder/AbortableStrategyValuesHolder.java create mode 100644 base/src/main/java/software/xdev/tci/startup/wait/strategy/AbstractWaitAbortableStrategy.java create mode 100644 base/src/main/java/software/xdev/tci/startup/wait/strategy/HostPortWaitAbortableStrategy.java create mode 100644 base/src/main/java/software/xdev/tci/startup/wait/strategy/HttpWaitAbortableStrategy.java create mode 100644 base/src/main/java/software/xdev/tci/startup/wait/strategy/LogMessageWaitAbortableStrategy.java create mode 100644 base/src/main/java/software/xdev/tci/startup/wait/strategy/WaitAllAbortableStrategy.java create mode 100644 base/src/main/java25/software/xdev/tci/startup/wait/holder/AbortableStrategyValuesHolder.java create mode 100644 mockserver/src/main/java/software/xdev/tci/mockserver/containers/TCIMockserverContainer.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 554eeb0a..28e2023b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,10 +8,15 @@ * `restore` - Restores the previously downloaded images * `setup-buildx` - Set's up the buildx worker and returns required environment variables * `save` - Saves the downloaded images +* Added abortable wait strategies + * primarily designed to be used with `FastAbortOnContainerDeathWaitStrategy` + * the strategies can be aborted e.g. when a container died during startup + * a dead container therefore no longer has to wait until the corresponding strategy times out * `db-jdbc-spring-orm` & implementations * No longer use `setPersistenceProviderClassName` because it has no effect * Performance: Reuse `PersistenceProvider` * Fixed a deadlock during service resolution +* Fixed rate limiting in some places * Improve container cleanup * Updated dependencies * Updated demo diff --git a/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/containers/WebAppContainer.java b/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/containers/WebAppContainer.java index 995a8faa..4b7bf5d0 100644 --- a/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/containers/WebAppContainer.java +++ b/advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/containers/WebAppContainer.java @@ -3,14 +3,15 @@ import java.time.Duration; import org.testcontainers.containers.GenericContainer; -import org.testcontainers.containers.wait.strategy.HttpWaitStrategy; -import org.testcontainers.containers.wait.strategy.WaitAllStrategy; import com.github.dockerjava.api.command.InspectContainerResponse; import com.github.dockerjava.zerodep.shaded.org.apache.hc.core5.http.HttpStatus; import software.xdev.tci.jacoco.containers.JaCoCoAwareContainer; import software.xdev.tci.startup.error.java.fatal.HsErrPidStartUpCrashReporter; +import software.xdev.tci.startup.wait.FastAbortOnContainerDeathWaitStrategy; +import software.xdev.tci.startup.wait.strategy.HostPortWaitAbortableStrategy; +import software.xdev.tci.startup.wait.strategy.HttpWaitAbortableStrategy; @SuppressWarnings("java:S2160") @@ -111,23 +112,22 @@ public WebAppContainer withDefaultWaitStrategy( final String actuatorUsername, final String actuatorPassword) { - return this.waitingFor(new WaitAllStrategy() + return this.waitingFor(FastAbortOnContainerDeathWaitStrategy.waitAll(s -> s .withStartupTimeout(startUpTimeout) - .withStrategy( - new HttpWaitStrategy() + .withStrategy(new HostPortWaitAbortableStrategy()) + .withStrategy(new HttpWaitAbortableStrategy() .forPort(WebAppContainer.DEFAULT_HTTP_PORT) .forPath("/robots.txt") .forStatusCode(HttpStatus.SC_OK) - .withReadTimeout(Duration.ofSeconds(10)) - ) - .withStrategy( - new HttpWaitStrategy() + .withReadTimeout(Duration.ofSeconds(10))) + .withStrategy(new HttpWaitAbortableStrategy() .forPort(WebAppContainer.DEFAULT_HTTP_PORT) .forPath("/actuator/health") .withBasicCredentials(actuatorUsername, actuatorPassword) .forStatusCode(HttpStatus.SC_OK) .withReadTimeout(Duration.ofSeconds(10)) - )); + )) + ); } @Override diff --git a/base/pom.xml b/base/pom.xml index cb5df55c..09c85580 100644 --- a/base/pom.xml +++ b/base/pom.xml @@ -136,6 +136,28 @@ org.apache.maven.plugins maven-compiler-plugin 3.15.0 + + + compile-java-21 + + compile + + + + compile-java-25 + compile + + compile + + + 25 + + ${project.basedir}/src/main/java25 + + true + + + ${maven.compiler.release} diff --git a/base/src/main/java/software/xdev/tci/startup/wait/AbortMonitor.java b/base/src/main/java/software/xdev/tci/startup/wait/AbortMonitor.java new file mode 100644 index 00000000..08723082 --- /dev/null +++ b/base/src/main/java/software/xdev/tci/startup/wait/AbortMonitor.java @@ -0,0 +1,45 @@ +/* + * Copyright © 2024 XDEV Software (https://xdev.software) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package software.xdev.tci.startup.wait; + +import java.util.concurrent.atomic.AtomicReference; + + +/** + * Monitor that holds the state if a WaitStrategy should be aborted + */ +public class AbortMonitor +{ + private final AtomicReference abortEx = new AtomicReference<>(); + + public void trigger(final RuntimeException rex) + { + this.abortEx.compareAndSet(null, rex); + } + + public void throwIfRequired() + { + if(this.shouldAbort()) + { + throw this.abortEx.get(); + } + } + + public boolean shouldAbort() + { + return this.abortEx.get() != null; + } +} diff --git a/base/src/main/java/software/xdev/tci/startup/wait/FastAbortOnContainerDeathWaitStrategy.java b/base/src/main/java/software/xdev/tci/startup/wait/FastAbortOnContainerDeathWaitStrategy.java new file mode 100644 index 00000000..694be3e7 --- /dev/null +++ b/base/src/main/java/software/xdev/tci/startup/wait/FastAbortOnContainerDeathWaitStrategy.java @@ -0,0 +1,166 @@ +/* + * Copyright © 2024 XDEV Software (https://xdev.software) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package software.xdev.tci.startup.wait; + +import java.time.Duration; +import java.util.Optional; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import java.util.function.UnaryOperator; + +import org.rnorth.ducttape.ratelimits.RateLimiter; +import org.rnorth.ducttape.ratelimits.RateLimiterBuilder; +import org.testcontainers.containers.wait.strategy.AbstractWaitStrategy; +import org.testcontainers.containers.wait.strategy.WaitStrategy; + +import software.xdev.tci.concurrent.ExecutorServiceCreatorHolder; +import software.xdev.tci.startup.wait.holder.AbortableStrategyValues; +import software.xdev.tci.startup.wait.holder.AbortableStrategyValuesHolder; +import software.xdev.tci.startup.wait.strategy.AbstractWaitAbortableStrategy; +import software.xdev.tci.startup.wait.strategy.WaitAllAbortableStrategy; + + +/** + * A "wrapper" strategy that causes a fast-abort when the container dies. + *

+ * This is helpful when the container dies during the startup phase as the default wait-strategies + * would wait for the timeout to expire which can take up to a few minutes. + *

+ */ +public class FastAbortOnContainerDeathWaitStrategy extends AbstractWaitStrategy +{ + protected static final ExecutorService DEFAULT_EXECUTOR = + ExecutorServiceCreatorHolder.instance().createUnlimited("TCI-testcontainers-wait"); + + protected static final RateLimiter DEFAULT_RATE_LIMITER = RateLimiterBuilder + .newBuilder() + .withRate(10, TimeUnit.MINUTES) + .withConstantThroughput() + .build(); + + protected final WaitStrategy waitStrategy; + protected final ExecutorService executor; + + public FastAbortOnContainerDeathWaitStrategy(final AbstractWaitAbortableStrategy waitStrategy) + { + this((WaitStrategy)waitStrategy); + } + + /** + * @apiNote It's recommended to use {@link #FastAbortOnContainerDeathWaitStrategy(AbstractWaitAbortableStrategy)} + */ + public FastAbortOnContainerDeathWaitStrategy(final WaitStrategy waitStrategy) + { + this(waitStrategy, DEFAULT_EXECUTOR); + } + + protected FastAbortOnContainerDeathWaitStrategy( + final WaitStrategy waitStrategy, + final ExecutorService executor) + { + this.waitStrategy = waitStrategy; + this.executor = executor; + + this.withRateLimiter(DEFAULT_RATE_LIMITER); + } + + @Override + protected void waitUntilReady() + { + final AbortMonitor abortMonitor = new AbortMonitor(); + final CompletableFuture cfWaitStrategy = CompletableFuture.runAsync( + () -> AbortableStrategyValuesHolder.executeWith( + () -> this.waitStrategy.waitUntilReady(this.waitStrategyTarget), + new AbortableStrategyValues(abortMonitor, this.executor)), + this.executor + ); + + final CompletableFuture cfContainerDeathWatchDog = CompletableFuture.runAsync( + () -> this.runCheckIfContainerIsDead(abortMonitor, cfWaitStrategy), + this.executor + ); + + try + { + CompletableFuture.anyOf(cfWaitStrategy, cfContainerDeathWatchDog).join(); + } + finally + { + abortMonitor.trigger(new CancellationException("Completed")); + } + } + + @SuppressWarnings("java:S5411") // Irrelevant here! + protected void runCheckIfContainerIsDead( + final AbortMonitor abortMonitor, + final CompletableFuture cfWaitStrategy) + { + while(!abortMonitor.shouldAbort() && !cfWaitStrategy.isDone()) + { + if(!this.rateLimiterWhenReadyExceptionless(this::isContainerAlive)) + { + final IllegalStateException abortEx = new IllegalStateException( + "Container " + this.waitStrategyTarget.getContainerId() + " is dead - Aborting"); + abortMonitor.trigger(abortEx); + throw abortEx; + } + } + } + + @SuppressWarnings("PMD.PreserveStackTrace") + protected T rateLimiterWhenReadyExceptionless(final Supplier supplier) + { + try + { + return this.getRateLimiter().getWhenReady(supplier::get); + } + catch(final Exception ex) + { + if(ex instanceof final RuntimeException rex) + { + throw rex; + } + throw new RuntimeException(ex); + } + } + + protected boolean isContainerAlive() + { + return Optional.ofNullable(this.waitStrategyTarget.getCurrentContainerInfo().getState()) + .filter(s -> Boolean.TRUE.equals(s.getRunning()) + || Boolean.TRUE.equals(s.getPaused())) + .isPresent(); + } + + @Override + public WaitStrategy withStartupTimeout(final Duration startupTimeout) + { + this.waitStrategy.withStartupTimeout(startupTimeout); + return this; + } + + /** + * Creates a new instance with {@link WaitAllAbortableStrategy} as inner strategy and allows configuring it. + */ + public static FastAbortOnContainerDeathWaitStrategy waitAll( + final UnaryOperator configure) + { + return new FastAbortOnContainerDeathWaitStrategy(configure.apply(new WaitAllAbortableStrategy())); + } +} diff --git a/base/src/main/java/software/xdev/tci/startup/wait/holder/AbortableStrategyValues.java b/base/src/main/java/software/xdev/tci/startup/wait/holder/AbortableStrategyValues.java new file mode 100644 index 00000000..97f5f2a3 --- /dev/null +++ b/base/src/main/java/software/xdev/tci/startup/wait/holder/AbortableStrategyValues.java @@ -0,0 +1,28 @@ +/* + * Copyright © 2024 XDEV Software (https://xdev.software) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package software.xdev.tci.startup.wait.holder; + +import java.util.concurrent.ExecutorService; + +import software.xdev.tci.startup.wait.AbortMonitor; + + +public record AbortableStrategyValues( + AbortMonitor abortMonitor, + ExecutorService executor +) +{ +} diff --git a/base/src/main/java/software/xdev/tci/startup/wait/holder/AbortableStrategyValuesHolder.java b/base/src/main/java/software/xdev/tci/startup/wait/holder/AbortableStrategyValuesHolder.java new file mode 100644 index 00000000..92f57a23 --- /dev/null +++ b/base/src/main/java/software/xdev/tci/startup/wait/holder/AbortableStrategyValuesHolder.java @@ -0,0 +1,46 @@ +/* + * Copyright © 2024 XDEV Software (https://xdev.software) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package software.xdev.tci.startup.wait.holder; + +/** + * @apiNote Please note that this is the Java 21 version. The Java 25 version uses ScopedValues. + */ +public final class AbortableStrategyValuesHolder +{ + private static final ThreadLocal TL = new ThreadLocal<>(); + + public static void executeWith(final Runnable runnable, final AbortableStrategyValues values) + { + TL.set(values); + try + { + runnable.run(); + } + finally + { + TL.remove(); + } + } + + public static AbortableStrategyValues get() + { + return TL.get(); + } + + private AbortableStrategyValuesHolder() + { + } +} diff --git a/base/src/main/java/software/xdev/tci/startup/wait/strategy/AbstractWaitAbortableStrategy.java b/base/src/main/java/software/xdev/tci/startup/wait/strategy/AbstractWaitAbortableStrategy.java new file mode 100644 index 00000000..5dd0be68 --- /dev/null +++ b/base/src/main/java/software/xdev/tci/startup/wait/strategy/AbstractWaitAbortableStrategy.java @@ -0,0 +1,75 @@ +/* + * Copyright © 2024 XDEV Software (https://xdev.software) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package software.xdev.tci.startup.wait.strategy; + +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; + +import org.rnorth.ducttape.unreliables.Unreliables; +import org.testcontainers.containers.wait.strategy.AbstractWaitStrategy; + +import software.xdev.tci.startup.wait.AbortMonitor; +import software.xdev.tci.startup.wait.holder.AbortableStrategyValuesHolder; + + +public abstract class AbstractWaitAbortableStrategy> + extends AbstractWaitStrategy +{ + protected T startupRetryUntilSuccess(final Callable callable) + { + return Unreliables.retryUntilSuccess( + (int)this.startupTimeout.getSeconds(), + TimeUnit.SECONDS, + callable + ); + } + + protected void startupRetryUntilSuccessWithRateLimitWhenNotAborted( + final AbortMonitor abortMonitor, + final Runnable runnable) + { + this.startupRetryUntilSuccess(() -> { + if(!abortMonitor.shouldAbort()) + { + this.getRateLimiter().doWhenReady(runnable); + } + return true; + }); + } + + @Override + protected void waitUntilReady() + { + this.waitUntilReady(AbortableStrategyValuesHolder.get().abortMonitor()); + } + + protected void waitUntilReady(final AbortMonitor abortMonitor) + { + // NOOP + } + + protected ExecutorService getExecutor() + { + return AbortableStrategyValuesHolder.get().executor(); + } + + @SuppressWarnings("unchecked") + protected S self() + { + return (S)this; + } +} diff --git a/base/src/main/java/software/xdev/tci/startup/wait/strategy/HostPortWaitAbortableStrategy.java b/base/src/main/java/software/xdev/tci/startup/wait/strategy/HostPortWaitAbortableStrategy.java new file mode 100644 index 00000000..8a048f64 --- /dev/null +++ b/base/src/main/java/software/xdev/tci/startup/wait/strategy/HostPortWaitAbortableStrategy.java @@ -0,0 +1,172 @@ +/* + * Copyright © 2024 XDEV Software (https://xdev.software) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package software.xdev.tci.startup.wait.strategy; + +import java.time.Duration; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.ContainerLaunchException; +import org.testcontainers.containers.wait.internal.ExternalPortListeningCheck; +import org.testcontainers.containers.wait.internal.InternalCommandPortListeningCheck; +import org.testcontainers.shaded.org.awaitility.Awaitility; + +import software.xdev.tci.startup.wait.AbortMonitor; + + +/** + * Based on {@link org.testcontainers.containers.wait.strategy.HostPortWaitStrategy} + */ +public class HostPortWaitAbortableStrategy extends AbstractWaitAbortableStrategy +{ + private static final Logger LOG = LoggerFactory.getLogger(HostPortWaitAbortableStrategy.class); + + protected int[] ports; + + @Override + protected void waitUntilReady(final AbortMonitor abortMonitor) + { + final Set externalLivenessCheckPorts; + if(this.ports == null || this.ports.length == 0) + { + externalLivenessCheckPorts = this.getLivenessCheckPorts(); + if(externalLivenessCheckPorts.isEmpty()) + { + if(LOG.isDebugEnabled()) + { + LOG.debug( + "Liveness check ports of {} is empty. Not waiting.", + this.waitStrategyTarget.getContainerInfo().getName() + ); + } + return; + } + } + else + { + externalLivenessCheckPorts = + Arrays + .stream(this.ports) + .mapToObj(port -> this.waitStrategyTarget.getMappedPort(port)) + .collect(Collectors.toSet()); + } + + abortMonitor.throwIfRequired(); + + final List exposedPorts = this.waitStrategyTarget.getExposedPorts(); + + final Set internalPorts = this.getInternalPorts(externalLivenessCheckPorts, exposedPorts); + + final Callable internalCheck = + new InternalCommandPortListeningCheck(this.waitStrategyTarget, internalPorts); + + final Callable externalCheck = new ExternalPortListeningCheck( + this.waitStrategyTarget, + externalLivenessCheckPorts + ); + + try + { + final List> futures = this.getExecutor().invokeAll( + Arrays.asList( + // Blocking + () -> { + final long startMs = System.currentTimeMillis(); + final Boolean result = internalCheck.call(); + LOG.debug( + "Internal port check {} for {} took {}ms", + Boolean.TRUE.equals(result) ? "passed" : "failed", + internalPorts, + System.currentTimeMillis() - startMs + ); + return result; + }, + // Polling + () -> { + final long startMs = System.currentTimeMillis(); + Awaitility.await() + .pollInSameThread() + .pollInterval(Duration.ofMillis(100)) + .pollDelay(Duration.ZERO) + .failFast( + "container is no longer running", + () -> !this.waitStrategyTarget.isRunning() || abortMonitor.shouldAbort()) + .ignoreExceptions() + .forever() + .until(externalCheck); + + LOG.debug( + "External port check passed for {} mapped as {} took {}ms", + internalPorts, + externalLivenessCheckPorts, + System.currentTimeMillis() - startMs + ); + return true; + } + ), + this.startupTimeout.getSeconds(), + TimeUnit.SECONDS + ); + + for(final Future future : futures) + { + future.get(0, TimeUnit.SECONDS); + } + } + catch(final InterruptedException iex) + { + Thread.currentThread().interrupt(); + throw new IllegalStateException("Got interrupted", iex); + } + catch(final CancellationException | ExecutionException | TimeoutException e) + { + throw new ContainerLaunchException( + "Timed out waiting for container port to open (" + + this.waitStrategyTarget.getHost() + + " ports: " + + externalLivenessCheckPorts + + " should be listening)", + e + ); + } + } + + protected Set getInternalPorts( + final Set externalLivenessCheckPorts, + final List exposedPorts) + { + return exposedPorts + .stream() + .filter(it -> externalLivenessCheckPorts.contains(this.waitStrategyTarget.getMappedPort(it))) + .collect(Collectors.toSet()); + } + + public HostPortWaitAbortableStrategy forPorts(final int... ports) + { + this.ports = ports; + return this.self(); + } +} diff --git a/base/src/main/java/software/xdev/tci/startup/wait/strategy/HttpWaitAbortableStrategy.java b/base/src/main/java/software/xdev/tci/startup/wait/strategy/HttpWaitAbortableStrategy.java new file mode 100644 index 00000000..42e951ad --- /dev/null +++ b/base/src/main/java/software/xdev/tci/startup/wait/strategy/HttpWaitAbortableStrategy.java @@ -0,0 +1,524 @@ +/* + * Copyright © 2024 XDEV Software (https://xdev.software) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package software.xdev.tci.startup.wait.strategy; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.UncheckedIOException; +import java.net.HttpURLConnection; +import java.net.Socket; +import java.net.URI; +import java.net.URL; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.cert.X509Certificate; +import java.time.Duration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509ExtendedTrustManager; + +import org.rnorth.ducttape.TimeoutException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.ContainerLaunchException; +import org.testcontainers.shaded.com.google.common.io.BaseEncoding; + +import software.xdev.tci.startup.wait.AbortMonitor; + + +/** + * Based on {@link org.testcontainers.containers.wait.strategy.HttpWaitStrategy} + */ +@SuppressWarnings("PMD.GodClass") +public class HttpWaitAbortableStrategy extends AbstractWaitAbortableStrategy +{ + private static final Logger LOG = LoggerFactory.getLogger(HttpWaitAbortableStrategy.class); + + /** + * Authorization HTTP header. + */ + protected static final String HEADER_AUTHORIZATION = "Authorization"; + + /** + * Basic Authorization scheme prefix. + */ + protected static final String AUTH_BASIC = "Basic "; + + protected String path = "/"; + + protected String method = "GET"; + + protected final Set statusCodes = new HashSet<>(); + + protected boolean tlsEnabled; + + protected String username; + + protected String password; + + protected final Map headers = new HashMap<>(); + + protected Predicate responsePredicate; + + protected Predicate statusCodePredicate; + + protected Optional livenessPort = Optional.empty(); + + protected Duration readTimeout = Duration.ofSeconds(1); + + protected boolean allowInsecure; + + // region Configure + + /** + * Waits for the given status code. + * + * @param statusCode the expected status code + * @return this + */ + public HttpWaitAbortableStrategy forStatusCode(final int statusCode) + { + this.statusCodes.add(statusCode); + return this.self(); + } + + /** + * Waits for the status code to pass the given predicate + * + * @param statusCodePredicate The predicate to test the response against + * @return this + */ + public HttpWaitAbortableStrategy forStatusCodeMatching(final Predicate statusCodePredicate) + { + this.statusCodePredicate = statusCodePredicate; + return this; + } + + /** + * Waits for the given path. + * + * @param path the path to check + * @return this + */ + public HttpWaitAbortableStrategy forPath(final String path) + { + this.path = path; + return this.self(); + } + + /** + * Wait for the given port. + * + * @param port the given port + * @return this + */ + public HttpWaitAbortableStrategy forPort(final int port) + { + this.livenessPort = Optional.of(port); + return this.self(); + } + + /** + * Indicates that the status check should use HTTPS. + * + * @return this + */ + public HttpWaitAbortableStrategy usingTls() + { + this.tlsEnabled = true; + return this.self(); + } + + /** + * Indicates the HTTP method to use (GET by default). + * + * @param method the HTTP method. + * @return this + */ + public HttpWaitAbortableStrategy withMethod(final String method) + { + this.method = method; + return this.self(); + } + + /** + * Indicates that HTTPS connection could use untrusted (self signed) certificate chains. + * + * @return this + */ + public HttpWaitAbortableStrategy allowInsecure() + { + this.allowInsecure = true; + return this.self(); + } + + /** + * Authenticate with HTTP Basic Authorization credentials. + * + * @param username the username + * @param password the password + * @return this + */ + public HttpWaitAbortableStrategy withBasicCredentials( + final String username, + final String password) + { + this.username = username; + this.password = password; + return this.self(); + } + + /** + * Add a custom HTTP Header to the call. + * + * @param name The HTTP Header name + * @param value The HTTP Header value + * @return this + */ + public HttpWaitAbortableStrategy withHeader( + final String name, + final String value) + { + this.headers.put(name, value); + return this.self(); + } + + /** + * Add multiple custom HTTP Headers to the call. + * + * @param headers Headers map of name/value + * @return this + */ + public HttpWaitAbortableStrategy withHeaders(final Map headers) + { + this.headers.putAll(headers); + return this.self(); + } + + /** + * Set the HTTP connections read timeout. + * + * @param timeout the timeout (minimum 1 millisecond) + * @return this + */ + public HttpWaitAbortableStrategy withReadTimeout(final Duration timeout) + { + if(timeout.toMillis() < 1) + { + throw new IllegalArgumentException("you cannot specify a value smaller than 1 ms"); + } + this.readTimeout = timeout; + return this.self(); + } + + /** + * Waits for the response to pass the given predicate + * + * @param responsePredicate The predicate to test the response against + * @return this + */ + public HttpWaitAbortableStrategy forResponsePredicate(final Predicate responsePredicate) + { + this.responsePredicate = responsePredicate; + return this.self(); + } + + // endregion + + @Override + protected void waitUntilReady(final AbortMonitor abortMonitor) + { + final String containerName = this.waitStrategyTarget.getContainerInfo().getName(); + + final Integer livenessCheckPort = this.livenessPort + .map(this.waitStrategyTarget::getMappedPort) + .orElseGet(() -> { + final Set livenessCheckPorts = this.getLivenessCheckPorts(); + if(livenessCheckPorts == null || livenessCheckPorts.isEmpty()) + { + LOG.warn("{}: No exposed ports or mapped ports - cannot wait for status", containerName); + return -1; + } + return livenessCheckPorts.iterator().next(); + }); + + abortMonitor.throwIfRequired(); + + if(null == livenessCheckPort || -1 == livenessCheckPort) + { + return; + } + final URI rawUri = this.buildLivenessUri(livenessCheckPort); + final String uri = rawUri.toString(); + + try + { + // Un-map the port for logging + final int originalPort = this.waitStrategyTarget + .getExposedPorts() + .stream() + .filter(exposedPort -> rawUri.getPort() == this.waitStrategyTarget.getMappedPort(exposedPort)) + .findFirst() + .orElseThrow(() -> new IllegalStateException("Target port " + rawUri.getPort() + " is not exposed")); + LOG.info( + "{}: Waiting for {} seconds for URL: {} (where port {} maps to container port {})", + containerName, + this.startupTimeout.getSeconds(), + uri, + rawUri.getPort(), + originalPort + ); + } + catch(final RuntimeException e) + { + // do not allow a failure in logging to prevent progress, but log for diagnosis + LOG.warn("Unexpected error occurred - will proceed to try to wait anyway", e); + } + + abortMonitor.throwIfRequired(); + + // try to connect to the URL + try + { + this.startupRetryUntilSuccessWithRateLimitWhenNotAborted( + abortMonitor, + () -> this.connectAndCheck(uri)); + abortMonitor.throwIfRequired(); + } + catch(final TimeoutException e) + { + throw new ContainerLaunchException( + String.format( + "Timed out waiting for URL to be accessible (%s should return HTTP %s)", + uri, + this.statusCodes.isEmpty() ? HttpURLConnection.HTTP_OK : this.statusCodes + ), + e + ); + } + } + + protected void connectAndCheck(final String uri) + { + try + { + final HttpURLConnection connection = this.openConnection(uri); + connection.setReadTimeout(Math.toIntExact(this.readTimeout.toMillis())); + + // authenticate + if(this.username != null && !this.username.isEmpty()) + { + connection.setRequestProperty( + HEADER_AUTHORIZATION, + this.buildAuthString(this.username, this.password) + ); + connection.setUseCaches(false); + } + + // Add user configured headers + this.headers.forEach(connection::setRequestProperty); + connection.setRequestMethod(this.method); + connection.connect(); + + LOG.trace("Get response code {}", connection.getResponseCode()); + + // Choose the statusCodePredicate strategy depending on what we defined. + if(!this.determineStatusCodePredicate().test(connection.getResponseCode())) + { + throw new IllegalStateException("HTTP response code was: " + connection.getResponseCode()); + } + + if(this.responsePredicate != null) + { + final String responseBody = this.getResponseBody(connection); + + LOG.trace("Got response {}", responseBody); + + if(!this.responsePredicate.test(responseBody)) + { + throw new IllegalStateException("Response: " + responseBody + " did not match predicate"); + } + } + } + catch(final IOException e) + { + throw new UncheckedIOException(e); + } + } + + private Predicate determineStatusCodePredicate() + { + if(this.statusCodes.isEmpty() && this.statusCodePredicate == null) + { + // We have no status code and no predicate so we expect a 200 OK response code + return responseCode -> HttpURLConnection.HTTP_OK == responseCode; + } + else if(!this.statusCodes.isEmpty() && this.statusCodePredicate == null) + { + // We use the default status predicate checker when we only have status codes + return this.statusCodes::contains; + } + else if(this.statusCodes.isEmpty()) + { + // We only have a predicate + return this.statusCodePredicate; + } + + // We have both predicate and status code + return this.statusCodePredicate.or(this.statusCodes::contains); + } + + protected HttpURLConnection openConnection(final String uri) throws IOException + { + if(!this.tlsEnabled) + { + return (HttpURLConnection)new URL(uri).openConnection(); + } + + final HttpsURLConnection connection = (HttpsURLConnection)new URL(uri).openConnection(); + if(this.allowInsecure) + { + // Create a trust manager that does not validate certificate chains + // and trust all certificates + final TrustManager[] trustAllCerts = new TrustManager[]{ + new X509ExtendedTrustManager() + { + @Override + public X509Certificate[] getAcceptedIssuers() + { + return new X509Certificate[0]; + } + + @Override + public void checkClientTrusted(final X509Certificate[] certs, final String authType) + { + } + + @Override + public void checkServerTrusted(final X509Certificate[] certs, final String authType) + { + } + + @Override + public void checkClientTrusted( + final X509Certificate[] chain, + final String authType, + final Socket socket) + { + } + + @Override + public void checkServerTrusted( + final X509Certificate[] chain, + final String authType, + final Socket socket) + { + } + + @Override + public void checkClientTrusted( + final X509Certificate[] chain, + final String authType, + final SSLEngine engine) + { + } + + @Override + public void checkServerTrusted( + final X509Certificate[] chain, + final String authType, + final SSLEngine engine) + { + } + }, + }; + + try + { + // Create custom SSL context and set the "trust all certificates" trust manager + final SSLContext sc = SSLContext.getInstance("SSL"); + sc.init(new KeyManager[0], trustAllCerts, new SecureRandom()); + connection.setSSLSocketFactory(sc.getSocketFactory()); + } + catch(final NoSuchAlgorithmException | KeyManagementException ex) + { + throw new IOException("Unable to create custom SSL factory instance", ex); + } + } + + return connection; + } + + /** + * Build the URI on which to check if the container is ready. + * + * @param livenessCheckPort the liveness port + * @return the liveness URI + */ + @SuppressWarnings("checkstyle:MagicNumber") + protected URI buildLivenessUri(final int livenessCheckPort) + { + final String scheme = (this.tlsEnabled ? "https" : "http") + "://"; + final String host = this.waitStrategyTarget.getHost(); + + final String portSuffix = + this.tlsEnabled && livenessCheckPort == 443 + || !this.tlsEnabled && livenessCheckPort == 80 + ? "" + : ":" + livenessCheckPort; + + return URI.create(scheme + host + portSuffix + this.path); + } + + /** + * @param username the username + * @param password the password + * @return a basic authentication string for the given credentials + */ + protected String buildAuthString(final String username, final String password) + { + return AUTH_BASIC + BaseEncoding.base64().encode((username + ":" + password).getBytes()); + } + + @SuppressWarnings({"PMD.AvoidStringBuilderOrBuffer", "checkstyle:MagicNumber"}) + protected String getResponseBody(final HttpURLConnection connection) throws IOException + { + final BufferedReader reader = new BufferedReader(new InputStreamReader( + connection.getResponseCode() >= 200 && connection.getResponseCode() < 300 + ? connection.getInputStream() + : connection.getErrorStream())); + + final StringBuilder builder = new StringBuilder(); + String line; + while((line = reader.readLine()) != null) + { + builder.append(line); + } + return builder.toString(); + } +} diff --git a/base/src/main/java/software/xdev/tci/startup/wait/strategy/LogMessageWaitAbortableStrategy.java b/base/src/main/java/software/xdev/tci/startup/wait/strategy/LogMessageWaitAbortableStrategy.java new file mode 100644 index 00000000..651d6ee9 --- /dev/null +++ b/base/src/main/java/software/xdev/tci/startup/wait/strategy/LogMessageWaitAbortableStrategy.java @@ -0,0 +1,101 @@ +/* + * Copyright © 2024 XDEV Software (https://xdev.software) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package software.xdev.tci.startup.wait.strategy; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Predicate; + +import org.testcontainers.containers.ContainerLaunchException; +import org.testcontainers.containers.output.FrameConsumerResultCallback; +import org.testcontainers.containers.output.OutputFrame; +import org.testcontainers.containers.output.WaitingConsumer; + +import com.github.dockerjava.api.command.LogContainerCmd; + +import software.xdev.tci.startup.wait.AbortMonitor; + + +/** + * Based on {@link org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy} + */ +public class LogMessageWaitAbortableStrategy extends AbstractWaitAbortableStrategy +{ + protected String regEx; + + protected int times = 1; + + @Override + protected void waitUntilReady(final AbortMonitor abortMonitor) + { + try(final LogContainerCmd cmd = this.waitStrategyTarget + .getDockerClient() + .logContainerCmd(this.waitStrategyTarget.getContainerId()) + .withFollowStream(true) + .withSince(0) + .withStdOut(true) + .withStdErr(true)) + { + try(final FrameConsumerResultCallback callback = new FrameConsumerResultCallback()) + { + final WaitingConsumer waitingConsumer = new WaitingConsumer(); + callback.addConsumer(OutputFrame.OutputType.STDOUT, waitingConsumer); + callback.addConsumer(OutputFrame.OutputType.STDERR, waitingConsumer); + + cmd.exec(callback); + + final Predicate waitPredicate = outputFrame -> { + abortMonitor.throwIfRequired(); + + // (?s) enables line terminator matching (equivalent to Pattern.DOTALL) + return outputFrame.getUtf8String().matches("(?s)" + this.regEx); + }; + try + { + waitingConsumer.waitUntil( + waitPredicate, + this.startupTimeout.getSeconds(), + TimeUnit.SECONDS, + this.times); + } + catch(final TimeoutException e) + { + throw new ContainerLaunchException( + "Timed out waiting for log output matching '" + this.regEx + "'", + e); + } + } + catch(final IOException ioe) + { + throw new UncheckedIOException(ioe); + } + } + } + + public LogMessageWaitAbortableStrategy withRegEx(final String regEx) + { + this.regEx = regEx; + return this.self(); + } + + public LogMessageWaitAbortableStrategy withTimes(final int times) + { + this.times = times; + return this.self(); + } +} diff --git a/base/src/main/java/software/xdev/tci/startup/wait/strategy/WaitAllAbortableStrategy.java b/base/src/main/java/software/xdev/tci/startup/wait/strategy/WaitAllAbortableStrategy.java new file mode 100644 index 00000000..eb69606b --- /dev/null +++ b/base/src/main/java/software/xdev/tci/startup/wait/strategy/WaitAllAbortableStrategy.java @@ -0,0 +1,132 @@ +/* + * Copyright © 2024 XDEV Software (https://xdev.software) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package software.xdev.tci.startup.wait.strategy; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.rnorth.ducttape.timeouts.Timeouts; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.wait.strategy.WaitAllStrategy; +import org.testcontainers.containers.wait.strategy.WaitStrategy; + +import software.xdev.tci.startup.wait.AbortMonitor; +import software.xdev.tci.startup.wait.holder.AbortableStrategyValues; +import software.xdev.tci.startup.wait.holder.AbortableStrategyValuesHolder; + + +/** + * Based on {@link WaitAllStrategy} + */ +public class WaitAllAbortableStrategy extends AbstractWaitAbortableStrategy +{ + private static final Logger LOG = LoggerFactory.getLogger(WaitAllAbortableStrategy.class); + + protected final WaitAllStrategy.Mode mode; + + protected final List strategies = new ArrayList<>(); + + public WaitAllAbortableStrategy() + { + this(WaitAllStrategy.Mode.WITH_OUTER_TIMEOUT); + } + + public WaitAllAbortableStrategy(final WaitAllStrategy.Mode mode) + { + this.mode = mode; + } + + @Override + protected void waitUntilReady(final AbortMonitor abortMonitor) + { + if(this.mode == WaitAllStrategy.Mode.WITH_INDIVIDUAL_TIMEOUTS_ONLY) + { + this.waitUntilNestedStrategiesAreReady(abortMonitor); + } + else + { + final AbortableStrategyValues valuesToPropagate = AbortableStrategyValuesHolder.get(); + Timeouts.doWithTimeout( + (int)this.startupTimeout.toMillis(), + TimeUnit.MILLISECONDS, + () -> AbortableStrategyValuesHolder.executeWith( + () -> this.waitUntilNestedStrategiesAreReady(abortMonitor), + valuesToPropagate) + ); + } + } + + private void waitUntilNestedStrategiesAreReady(final AbortMonitor abortMonitor) + { + for(final WaitStrategy strategy : this.strategies) + { + abortMonitor.throwIfRequired(); + + final long startMs = System.currentTimeMillis(); + LOG.debug("Waiting for {}", strategy); + + strategy.waitUntilReady(this.waitStrategyTarget); + + LOG.debug("Finished waiting for {}, took {}ms", strategy, System.currentTimeMillis() - startMs); + } + } + + public WaitAllAbortableStrategy withStrategy(final AbstractWaitAbortableStrategy strategy) + { + return this.withWaitStrategy(strategy); + } + + /** + * @apiNote It's recommended to use {@link #withStrategy(AbstractWaitAbortableStrategy)} + */ + public WaitAllAbortableStrategy withWaitStrategy(final WaitStrategy strategy) + { + if(this.mode == WaitAllStrategy.Mode.WITH_OUTER_TIMEOUT) + { + this.applyStartupTimeout(strategy); + } + + this.strategies.add(strategy); + return this.self(); + } + + @Override + public WaitAllAbortableStrategy withStartupTimeout(final Duration startupTimeout) + { + if(this.mode == WaitAllStrategy.Mode.WITH_INDIVIDUAL_TIMEOUTS_ONLY) + { + throw new IllegalStateException( + String.format( + "Changing startup timeout is not supported with mode %s", + WaitAllStrategy.Mode.WITH_INDIVIDUAL_TIMEOUTS_ONLY + ) + ); + } + + super.withStartupTimeout(startupTimeout); + + this.strategies.forEach(this::applyStartupTimeout); + return this.self(); + } + + private void applyStartupTimeout(final WaitStrategy childStrategy) + { + childStrategy.withStartupTimeout(this.startupTimeout); + } +} diff --git a/base/src/main/java25/software/xdev/tci/startup/wait/holder/AbortableStrategyValuesHolder.java b/base/src/main/java25/software/xdev/tci/startup/wait/holder/AbortableStrategyValuesHolder.java new file mode 100644 index 00000000..d0c2b249 --- /dev/null +++ b/base/src/main/java25/software/xdev/tci/startup/wait/holder/AbortableStrategyValuesHolder.java @@ -0,0 +1,21 @@ +package software.xdev.tci.startup.wait.holder; + + +public final class AbortableStrategyValuesHolder +{ + private static final ScopedValue SV = ScopedValue.newInstance(); + + public static void executeWith(final Runnable runnable, final AbortableStrategyValues values) + { + ScopedValue.where(SV, values).run(runnable); + } + + public static AbortableStrategyValues get() + { + return SV.get(); + } + + private AbortableStrategyValuesHolder() + { + } +} diff --git a/db-jdbc/src/main/java/software/xdev/tci/db/containers/WaitableJDBCContainer.java b/db-jdbc/src/main/java/software/xdev/tci/db/containers/WaitableJDBCContainer.java index 2decd1cf..62ca1a55 100644 --- a/db-jdbc/src/main/java/software/xdev/tci/db/containers/WaitableJDBCContainer.java +++ b/db-jdbc/src/main/java/software/xdev/tci/db/containers/WaitableJDBCContainer.java @@ -16,22 +16,22 @@ package software.xdev.tci.db.containers; import java.sql.Connection; +import java.sql.SQLException; import java.time.Duration; import java.util.concurrent.TimeUnit; -import org.rnorth.ducttape.TimeoutException; import org.rnorth.ducttape.ratelimits.RateLimiter; import org.rnorth.ducttape.ratelimits.RateLimiterBuilder; import org.rnorth.ducttape.unreliables.Unreliables; -import org.testcontainers.containers.ContainerLaunchException; import org.testcontainers.containers.JdbcDatabaseContainer; -import org.testcontainers.containers.wait.strategy.AbstractWaitStrategy; -import org.testcontainers.containers.wait.strategy.Wait; -import org.testcontainers.containers.wait.strategy.WaitAllStrategy; import org.testcontainers.containers.wait.strategy.WaitStrategy; import org.testcontainers.containers.wait.strategy.WaitStrategyTarget; import software.xdev.tci.envperf.EnvironmentPerformance; +import software.xdev.tci.startup.wait.AbortMonitor; +import software.xdev.tci.startup.wait.FastAbortOnContainerDeathWaitStrategy; +import software.xdev.tci.startup.wait.strategy.AbstractWaitAbortableStrategy; +import software.xdev.tci.startup.wait.strategy.HostPortWaitAbortableStrategy; public interface WaitableJDBCContainer extends WaitStrategyTarget @@ -39,10 +39,11 @@ public interface WaitableJDBCContainer extends WaitStrategyTarget @SuppressWarnings("checkstyle:MagicNumber") default WaitStrategy completeJDBCWaitStrategy() { - return new WaitAllStrategy() - .withStrategy(Wait.defaultWaitStrategy()) + return FastAbortOnContainerDeathWaitStrategy.waitAll(s -> s + .withStartupTimeout(Duration.ofSeconds(40L + EnvironmentPerformance.cpuSlownessFactor() * 20L)) + .withStrategy(new HostPortWaitAbortableStrategy()) .withStrategy(new JDBCWaitStrategy()) - .withStartupTimeout(Duration.ofSeconds(40L + EnvironmentPerformance.cpuSlownessFactor() * 20L)); + ); } WaitStrategy getWaitStrategy(); @@ -59,20 +60,19 @@ default void waitUntilContainerStarted() /** * @apiNote Assumes that the container is already started */ - class JDBCWaitStrategy extends AbstractWaitStrategy + class JDBCWaitStrategy extends AbstractWaitAbortableStrategy { @SuppressWarnings("checkstyle:MagicNumber") public JDBCWaitStrategy() { this.withRateLimiter(RateLimiterBuilder.newBuilder() - .withRate(200, TimeUnit.MILLISECONDS) + .withRate(5, TimeUnit.SECONDS) .withConstantThroughput() .build()); } - @SuppressWarnings("PMD.PreserveStackTrace") @Override - protected void waitUntilReady() + protected void waitUntilReady(final AbortMonitor abortMonitor) { if(!(this.waitStrategyTarget instanceof final JdbcDatabaseContainer container)) { @@ -80,45 +80,33 @@ protected void waitUntilReady() "Container must implement JdbcDatabaseContainer and WaitableJDBCContainer"); } - try - { - this.waitUntilJDBCValid(container); - } - catch(final TimeoutException e) - { - throw new ContainerLaunchException( - "JDBCContainer cannot be accessed by (JDBC URL: " - + container.getJdbcUrl() - + "), please check container logs"); - } - } - - protected void waitUntilJDBCValid(final JdbcDatabaseContainer container) - { - Unreliables.retryUntilTrue( - (int)this.startupTimeout.getSeconds(), - TimeUnit.SECONDS, - // Rate limit creation of connections as this is quite an expensive operation - () -> this.getRateLimiter().getWhenReady(() -> { + // Rate limit creation of connections as this is quite an expensive operation + this.startupRetryUntilSuccessWithRateLimitWhenNotAborted( + abortMonitor, + () -> { try(final Connection connection = container.createConnection("")) { - return this.waitUntilJDBCConnectionValidated(container, connection); + this.waitUntilJDBCConnectionValidated(container, connection); + } + catch(final SQLException sqlEx) + { + throw new IllegalStateException("SQL failed", sqlEx); } - }) + } ); } @SuppressWarnings({"unused", "checkstyle:MagicNumber"}) // Parameter might be used by extension - protected boolean waitUntilJDBCConnectionValidated( + protected void waitUntilJDBCConnectionValidated( final JdbcDatabaseContainer container, final Connection connection) { final RateLimiter validateJDBCRateLimiter = RateLimiterBuilder.newBuilder() - .withRate(50, TimeUnit.MILLISECONDS) + .withRate(20, TimeUnit.SECONDS) .withConstantThroughput() .build(); - return Unreliables.retryUntilSuccess( + Unreliables.retryUntilSuccess( // If this fails after startupTimeout / 2 (min=3s, max=30s) // it might be possible that the connection is somehow corrupted or broken // -> Build a new connection after that (see waitUntilJDBCValid) diff --git a/mockserver/src/main/java/software/xdev/tci/mockserver/containers/TCIMockserverContainer.java b/mockserver/src/main/java/software/xdev/tci/mockserver/containers/TCIMockserverContainer.java new file mode 100644 index 00000000..50963957 --- /dev/null +++ b/mockserver/src/main/java/software/xdev/tci/mockserver/containers/TCIMockserverContainer.java @@ -0,0 +1,82 @@ +/* + * Copyright © 2025 XDEV Software (https://xdev.software) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package software.xdev.tci.mockserver.containers; + +import org.testcontainers.images.RemoteDockerImage; +import org.testcontainers.utility.DockerImageName; + +import com.github.dockerjava.api.command.InspectContainerResponse; + +import software.xdev.tci.misc.ContainerMemory; +import software.xdev.tci.startup.error.java.fatal.HsErrPidStartUpCrashReporter; +import software.xdev.tci.startup.wait.FastAbortOnContainerDeathWaitStrategy; +import software.xdev.tci.startup.wait.strategy.LogMessageWaitAbortableStrategy; +import software.xdev.testcontainers.mockserver.containers.MockServerContainer; + + +@SuppressWarnings("java:S2160") +public class TCIMockserverContainer extends MockServerContainer +{ + private final HsErrPidStartUpCrashReporter hsErrPidStartUpCrashReporter; + + public TCIMockserverContainer(final RemoteDockerImage image) + { + super(image); + this.hsErrPidStartUpCrashReporter = new HsErrPidStartUpCrashReporter(this); + } + + public TCIMockserverContainer(final DockerImageName dockerImageName) + { + super(dockerImageName); + this.hsErrPidStartUpCrashReporter = new HsErrPidStartUpCrashReporter(this); + } + + public TCIMockserverContainer(final String tag) + { + super(tag); + this.hsErrPidStartUpCrashReporter = new HsErrPidStartUpCrashReporter(this); + } + + public TCIMockserverContainer() + { + this.hsErrPidStartUpCrashReporter = new HsErrPidStartUpCrashReporter(this); + } + + @Override + protected void containerIsStarted(final InspectContainerResponse containerInfo, final boolean reused) + { + this.hsErrPidStartUpCrashReporter.containerIsStarted(); + super.containerIsStarted(containerInfo, reused); + } + + @Override + protected void containerIsStopping(final InspectContainerResponse containerInfo) + { + this.hsErrPidStartUpCrashReporter.containerIsStopping(this.logger()); + super.containerIsStopping(containerInfo); + } + + public static TCIMockserverContainer createDefaultForFactory() + { + final TCIMockserverContainer container = new TCIMockserverContainer(); + container + .withCreateContainerCmdModifier(cmd -> cmd.getHostConfig().withMemory(ContainerMemory.M512M)) + .waitingFor(new FastAbortOnContainerDeathWaitStrategy(new LogMessageWaitAbortableStrategy() + .withRegEx(".*started on port: " + PORT + ".*") + )); + return container; + } +} diff --git a/mockserver/src/main/java/software/xdev/tci/mockserver/factory/MockServerTCIFactory.java b/mockserver/src/main/java/software/xdev/tci/mockserver/factory/MockServerTCIFactory.java index d3d8b25e..5cb0bf03 100644 --- a/mockserver/src/main/java/software/xdev/tci/mockserver/factory/MockServerTCIFactory.java +++ b/mockserver/src/main/java/software/xdev/tci/mockserver/factory/MockServerTCIFactory.java @@ -22,8 +22,8 @@ import org.testcontainers.containers.Network; import software.xdev.tci.factory.ondemand.OnDemandTCIFactory; -import software.xdev.tci.misc.ContainerMemory; import software.xdev.tci.mockserver.MockServerTCI; +import software.xdev.tci.mockserver.containers.TCIMockserverContainer; import software.xdev.testcontainers.mockserver.containers.MockServerContainer; @@ -39,7 +39,6 @@ protected MockServerTCIFactory( super(infraBuilder, containerBuilder, containerBaseName, containerLoggerName); } - @SuppressWarnings("resource") protected MockServerTCIFactory( final BiFunction infraBuilder, final String additionalContainerBaseName, @@ -47,8 +46,7 @@ protected MockServerTCIFactory( { super( infraBuilder, - () -> new MockServerContainer() - .withCreateContainerCmdModifier(cmd -> cmd.getHostConfig().withMemory(ContainerMemory.M512M)), + TCIMockserverContainer::createDefaultForFactory, "mockserver-" + additionalContainerBaseName, "container.mockserver." + additionalLoggerName); } diff --git a/mockserver/src/main/java/software/xdev/tci/mockserver/factory/PreStartableMockServerTCIFactory.java b/mockserver/src/main/java/software/xdev/tci/mockserver/factory/PreStartableMockServerTCIFactory.java index 14d0346b..465ff0ee 100644 --- a/mockserver/src/main/java/software/xdev/tci/mockserver/factory/PreStartableMockServerTCIFactory.java +++ b/mockserver/src/main/java/software/xdev/tci/mockserver/factory/PreStartableMockServerTCIFactory.java @@ -18,15 +18,14 @@ import java.util.function.BiFunction; import software.xdev.tci.factory.prestart.PreStartableTCIFactory; -import software.xdev.tci.misc.ContainerMemory; import software.xdev.tci.mockserver.MockServerTCI; +import software.xdev.tci.mockserver.containers.TCIMockserverContainer; import software.xdev.testcontainers.mockserver.containers.MockServerContainer; public abstract class PreStartableMockServerTCIFactory extends PreStartableTCIFactory { - @SuppressWarnings("resource") protected PreStartableMockServerTCIFactory( final BiFunction infraBuilder, final String additionalContainerBaseName, @@ -35,8 +34,7 @@ protected PreStartableMockServerTCIFactory( { super( infraBuilder, - () -> new MockServerContainer() - .withCreateContainerCmdModifier(cmd -> cmd.getHostConfig().withMemory(ContainerMemory.M512M)), + TCIMockserverContainer::createDefaultForFactory, "mockserver-" + additionalContainerBaseName, "container.mockserver." + additionalLoggerName, prestartName); diff --git a/oidc-server-mock/src/main/java/software/xdev/tci/oidc/factory/BaseOIDCTCIFactory.java b/oidc-server-mock/src/main/java/software/xdev/tci/oidc/factory/BaseOIDCTCIFactory.java index 62a55a75..37a1e49a 100644 --- a/oidc-server-mock/src/main/java/software/xdev/tci/oidc/factory/BaseOIDCTCIFactory.java +++ b/oidc-server-mock/src/main/java/software/xdev/tci/oidc/factory/BaseOIDCTCIFactory.java @@ -21,9 +21,6 @@ import java.util.function.Supplier; import org.apache.hc.core5.http.HttpStatus; -import org.testcontainers.containers.wait.strategy.HostPortWaitStrategy; -import org.testcontainers.containers.wait.strategy.HttpWaitStrategy; -import org.testcontainers.containers.wait.strategy.WaitAllStrategy; import software.xdev.tci.envperf.EnvironmentPerformance; import software.xdev.tci.factory.prestart.PreStartableTCIFactory; @@ -32,6 +29,9 @@ import software.xdev.tci.oidc.BaseOIDCTCI; import software.xdev.tci.oidc.containers.BaseOIDCServerContainer; import software.xdev.tci.oidc.containers.OIDCServerContainer; +import software.xdev.tci.startup.wait.FastAbortOnContainerDeathWaitStrategy; +import software.xdev.tci.startup.wait.strategy.HostPortWaitAbortableStrategy; +import software.xdev.tci.startup.wait.strategy.HttpWaitAbortableStrategy; @SuppressWarnings("java:S119") @@ -92,22 +92,21 @@ public static OIDCServerContainer createDefaultContainer() return createDefaultContainer(null); } - @SuppressWarnings({"resource", "checkstyle:MagicNumber"}) + @SuppressWarnings({"checkstyle:MagicNumber"}) public static OIDCServerContainer createDefaultContainer(final Consumer customizer) { final OIDCServerContainer oidcServerContainer = new OIDCServerContainer() .withCreateContainerCmdModifier(cmd -> cmd.getHostConfig().withMemory(ContainerMemory.M512M)) - .waitingFor( - new WaitAllStrategy() - .withStartupTimeout(Duration.ofSeconds(40L + 20L * EnvironmentPerformance.cpuSlownessFactor())) - .withStrategy(new HostPortWaitStrategy()) - .withStrategy( - new HttpWaitStrategy() - .forPort(BaseOIDCServerContainer.PORT) - .forPath("/") - .forStatusCode(HttpStatus.SC_OK) - .withReadTimeout(Duration.ofSeconds(10)) - ) + .waitingFor(FastAbortOnContainerDeathWaitStrategy.waitAll(s -> s + .withStartupTimeout(Duration.ofSeconds(40L + 20L * EnvironmentPerformance.cpuSlownessFactor())) + .withStrategy(new HostPortWaitAbortableStrategy()) + .withStrategy( + new HttpWaitAbortableStrategy() + .forPort(BaseOIDCServerContainer.PORT) + .forPath("/") + .forStatusCode(HttpStatus.SC_OK) + .withReadTimeout(Duration.ofSeconds(10)) + )) ); if(customizer != null) diff --git a/selenium/src/main/java/software/xdev/tci/selenium/factory/BrowserTCIFactory.java b/selenium/src/main/java/software/xdev/tci/selenium/factory/BrowserTCIFactory.java index 055484ae..c98840e4 100644 --- a/selenium/src/main/java/software/xdev/tci/selenium/factory/BrowserTCIFactory.java +++ b/selenium/src/main/java/software/xdev/tci/selenium/factory/BrowserTCIFactory.java @@ -31,9 +31,6 @@ import org.rnorth.ducttape.unreliables.Unreliables; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.testcontainers.containers.wait.strategy.HostPortWaitStrategy; -import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy; -import org.testcontainers.containers.wait.strategy.WaitAllStrategy; import org.testcontainers.images.RemoteDockerImage; import software.xdev.tci.concurrent.TCIExecutorServiceHolder; @@ -45,6 +42,9 @@ import software.xdev.tci.selenium.containers.SeleniumBrowserWebDriverContainer; import software.xdev.tci.selenium.factory.config.BrowserTCIFactoryConfig; import software.xdev.tci.serviceloading.TCIServiceLoaderHolder; +import software.xdev.tci.startup.wait.FastAbortOnContainerDeathWaitStrategy; +import software.xdev.tci.startup.wait.strategy.HostPortWaitAbortableStrategy; +import software.xdev.tci.startup.wait.strategy.LogMessageWaitAbortableStrategy; import software.xdev.testcontainers.selenium.containers.browser.BrowserWebDriverContainer; import software.xdev.testcontainers.selenium.containers.recorder.SeleniumRecordingContainer; @@ -119,12 +119,11 @@ public static SeleniumBrowserWebDriverContainer createDefaultContainer( // https://github.com/SeleniumHQ/docker-selenium/issues/2355 .withEnv("SE_ENABLE_TRACING", "false") // Some (AWS) CPUs are completely overloaded with the default 15s timeout -> increase it - .waitingFor(new WaitAllStrategy() - .withStrategy(new LogMessageWaitStrategy() - .withRegEx(".*(Started Selenium Standalone).*\n") - .withStartupTimeout(Duration.ofSeconds(30 + 20L * cpuSlownessFactor()))) - .withStrategy(new HostPortWaitStrategy()) - .withStartupTimeout(Duration.ofSeconds(30 + 20L * cpuSlownessFactor()))); + .waitingFor(FastAbortOnContainerDeathWaitStrategy.waitAll(s -> s + .withStartupTimeout(Duration.ofSeconds(30 + 20L * cpuSlownessFactor())) + .withStrategy(new LogMessageWaitAbortableStrategy().withRegEx(".*(Started Selenium Standalone).*\n")) + .withStrategy(new HostPortWaitAbortableStrategy()) + )); } public static SeleniumRecordingContainer createDefaultRecordingContainer( From 826463d5ec4e88996c920bc8c8a478ab680bb83a Mon Sep 17 00:00:00 2001 From: AB Date: Tue, 14 Jul 2026 14:11:36 +0200 Subject: [PATCH 113/141] [Selenium] Re-use existing constants --- selenium/pom.xml | 2 +- .../xdev/tci/selenium/factory/BrowserTCIFactory.java | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/selenium/pom.xml b/selenium/pom.xml index 137ba8ed..1119fe4a 100644 --- a/selenium/pom.xml +++ b/selenium/pom.xml @@ -82,7 +82,7 @@ software.xdev testcontainers-selenium - 2.0.1 + 2.0.2 diff --git a/selenium/src/main/java/software/xdev/tci/selenium/factory/BrowserTCIFactory.java b/selenium/src/main/java/software/xdev/tci/selenium/factory/BrowserTCIFactory.java index c98840e4..65235342 100644 --- a/selenium/src/main/java/software/xdev/tci/selenium/factory/BrowserTCIFactory.java +++ b/selenium/src/main/java/software/xdev/tci/selenium/factory/BrowserTCIFactory.java @@ -121,7 +121,8 @@ public static SeleniumBrowserWebDriverContainer createDefaultContainer( // Some (AWS) CPUs are completely overloaded with the default 15s timeout -> increase it .waitingFor(FastAbortOnContainerDeathWaitStrategy.waitAll(s -> s .withStartupTimeout(Duration.ofSeconds(30 + 20L * cpuSlownessFactor())) - .withStrategy(new LogMessageWaitAbortableStrategy().withRegEx(".*(Started Selenium Standalone).*\n")) + .withStrategy(new LogMessageWaitAbortableStrategy() + .withRegEx(BrowserWebDriverContainer.LOG_MSG_WAIT_STRATEGY_REGEX)) .withStrategy(new HostPortWaitAbortableStrategy()) )); } @@ -132,7 +133,11 @@ public static SeleniumRecordingContainer createDefaultRecordingContainer( { return new SeleniumRecordingContainer(browserContainer) .withLogConsumer(getLogConsumer("container.browserrecorder." + capabilities.getBrowserName())) - .withCreateContainerCmdModifier(cmd -> cmd.getHostConfig().withMemory(ContainerMemory.M512M)); + .withCreateContainerCmdModifier(cmd -> cmd.getHostConfig().withMemory(ContainerMemory.M512M)) + .waitingFor(new FastAbortOnContainerDeathWaitStrategy( + new LogMessageWaitAbortableStrategy() + .withRegEx(SeleniumRecordingContainer.LOG_MSG_WAIT_STRATEGY_REGEX) + )); } public BrowserTCIFactory( From fcc6bb8a7de05dcc708934602c5d57db6bc08897 Mon Sep 17 00:00:00 2001 From: AB Date: Tue, 14 Jul 2026 14:20:08 +0200 Subject: [PATCH 114/141] Update build to use Java 25 --- .github/workflows/check-build.yml | 6 +++--- .github/workflows/release.yml | 8 ++++---- .github/workflows/run-integration-tests.yml | 8 ++++---- .github/workflows/test-deploy.yml | 4 ++-- jul-to-slf4j/README.md | 2 +- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/check-build.yml b/.github/workflows/check-build.yml index 3db742c4..127dc248 100644 --- a/.github/workflows/check-build.yml +++ b/.github/workflows/check-build.yml @@ -28,7 +28,7 @@ jobs: timeout-minutes: 30 strategy: matrix: - java: [21, 25] + java: [25] distribution: [temurin] steps: - uses: actions/checkout@v7 @@ -74,7 +74,7 @@ jobs: timeout-minutes: 15 strategy: matrix: - java: [21] + java: [25] distribution: [temurin] steps: - uses: actions/checkout@v7 @@ -110,7 +110,7 @@ jobs: timeout-minutes: 15 strategy: matrix: - java: [21] + java: [25] distribution: [temurin] steps: - uses: actions/checkout@v7 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9968f3d5..f1bf26d9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ jobs: - name: Set up JDK uses: actions/setup-java@v5 with: - java-version: '21' + java-version: '25' distribution: 'temurin' # Try to reuse existing cache from check-build @@ -125,7 +125,7 @@ jobs: uses: actions/setup-java@v5 with: # running setup-java overwrites the settings.xml distribution: 'temurin' - java-version: '21' + java-version: '25' server-id: github-central server-password: PACKAGES_CENTRAL_TOKEN gpg-passphrase: MAVEN_GPG_PASSPHRASE @@ -147,7 +147,7 @@ jobs: uses: actions/setup-java@v5 with: # running setup-java again overwrites the settings.xml distribution: 'temurin' - java-version: '21' + java-version: '25' server-id: sonatype-central-portal server-username: MAVEN_CENTRAL_USERNAME server-password: MAVEN_CENTRAL_TOKEN @@ -182,7 +182,7 @@ jobs: - name: Setup - Java uses: actions/setup-java@v5 with: - java-version: '21' + java-version: '25' distribution: 'temurin' # Try to reuse existing cache from check-build diff --git a/.github/workflows/run-integration-tests.yml b/.github/workflows/run-integration-tests.yml index fd0ea1ce..ec93a1cf 100644 --- a/.github/workflows/run-integration-tests.yml +++ b/.github/workflows/run-integration-tests.yml @@ -31,12 +31,12 @@ jobs: project: [persistence-it-hibernate] parallel: [0, 2] pre-start: [false, true] - java: [21] + java: [25] include: - project: persistence-it-eclipselink parallel: 2 pre-start: true - java: 21 + java: 25 name: "persistence (${{matrix.project}}, ${{matrix.parallel}}, ${{matrix.pre-start}}, ${{matrix.java}})" runs-on: ubuntu-latest if: ${{ !(github.event_name == 'pull_request' && startsWith(github.head_ref, 'renovate/')) }} @@ -102,14 +102,14 @@ jobs: project: [webapp-it] parallel: [0, 2] pre-start: [false, true] - java: [21] + java: [25] jacoco: [false] # Dedicated build with JaCoCo include: - project: webapp-it parallel: 2 pre-start: true - java: 21 + java: 25 jacoco: true name: "selenium (${{matrix.project}}, ${{matrix.parallel }}, ${{matrix.pre-start}}, ${{matrix.java}}${{ matrix.jacoco && ', JaCoCo' || '' }})" runs-on: ubuntu-latest diff --git a/.github/workflows/test-deploy.yml b/.github/workflows/test-deploy.yml index 2042eec9..570e1a33 100644 --- a/.github/workflows/test-deploy.yml +++ b/.github/workflows/test-deploy.yml @@ -14,7 +14,7 @@ jobs: uses: actions/setup-java@v5 with: # running setup-java overwrites the settings.xml distribution: 'temurin' - java-version: '21' + java-version: '25' server-id: github-central server-password: PACKAGES_CENTRAL_TOKEN gpg-passphrase: MAVEN_GPG_PASSPHRASE @@ -36,7 +36,7 @@ jobs: uses: actions/setup-java@v5 with: # running setup-java again overwrites the settings.xml distribution: 'temurin' - java-version: '21' + java-version: '25' server-id: sonatype-central-portal server-username: MAVEN_CENTRAL_USERNAME server-password: MAVEN_CENTRAL_TOKEN diff --git a/jul-to-slf4j/README.md b/jul-to-slf4j/README.md index d6074722..3959daa0 100644 --- a/jul-to-slf4j/README.md +++ b/jul-to-slf4j/README.md @@ -1,3 +1,3 @@ # JUL to SLF4J -Logging Adapter to redirect [JUL](https://docs.oracle.com/en/java/javase/21/docs/api/java.logging/java/util/logging/package-summary.html) to [SLF4J](https://github.com/qos-ch/slf4j) +Logging Adapter to redirect [JUL](https://docs.oracle.com/en/java/javase/25/docs/api/java.logging/java/util/logging/package-summary.html) to [SLF4J](https://github.com/qos-ch/slf4j) From 49751b3815445dc8c67e9da01d524b311b323c61 Mon Sep 17 00:00:00 2001 From: AB Date: Tue, 14 Jul 2026 14:35:41 +0200 Subject: [PATCH 115/141] Improve docs --- base/README.md | 10 ++++++---- .../error/java/fatal/HsErrPidStartUpCrashReporter.java | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/base/README.md b/base/README.md index 866eeab8..78ad3563 100644 --- a/base/README.md +++ b/base/README.md @@ -6,13 +6,15 @@ Base for other modules and core components. | Feature | Why? | Demo | | --- | --- | --- | -| Easily create infrastructure using TCI[JD](https://javadoc.io/doc/software.xdev/base/latest/software/xdev/tci/TCI.html) (TestContainer Infrastructure) templating + Factories for that | Makes writing and designing tests easier | [here](../base-demo/src/test/java/software/xdev/tci/dummyinfra/) | -| [PreStarting mechanism](./src/main/java/software/xdev/tci/factory/prestart/)[JD](https://javadoc.io/doc/software.xdev/base/latest/software/xdev/tci/factory/prestart/PreStartableTCIFactory.html) for [additional performance](../PERFORMANCE.md) | Tries to run tests as fast as possible - with a few trade-offs | [here](../base-demo/src/test/java/software/xdev/tci/factory/prestart/) | +| Easily create infrastructure using TCI[JD](https://javadoc.io/doc/software.xdev.tci/base/latest/software/xdev/tci/TCI.html) (TestContainer Infrastructure) templating + Factories for that | Makes writing and designing tests easier | [here](../base-demo/src/test/java/software/xdev/tci/dummyinfra/) | +| [PreStarting mechanism](./src/main/java/software/xdev/tci/factory/prestart/)[JD](https://javadoc.io/doc/software.xdev.tci/base/latest/software/xdev/tci/factory/prestart/PreStartableTCIFactory.html) for [additional performance](../PERFORMANCE.md) | Tries to run tests as fast as possible - with a few trade-offs | [here](../base-demo/src/test/java/software/xdev/tci/factory/prestart/) | | All started containers have a unique human-readable name | Easier identification when tracing or debugging | [here](../base-demo/src/test/java/software/xdev/tci/safestart/) | -| An optimized [implementation of Network](./src/main/java/software/xdev/tci/network/)[JD](https://javadoc.io/doc/software.xdev/base/latest/software/xdev/tci/network/LazyNetwork.html) | Addresses various problems of the original implementation to speed up tests | [here](../base-demo/src/test/java/software/xdev/tci/network/) | -| [Safe starting of named containers](./src/main/java/software/xdev/tci/safestart/)[JD](https://javadoc.io/doc/software.xdev/base/latest/software/xdev/tci/safestart/SafeNamedContainerStarter.html) | Ensures that a container doesn't enter a crash loop during retried startups | [here](../base-demo/src/test/java/software/xdev/tci/safestart/) | +| An optimized [implementation of Network](./src/main/java/software/xdev/tci/network/)[JD](https://javadoc.io/doc/software.xdev.tci/base/latest/software/xdev/tci/network/LazyNetwork.html) | Addresses various problems of the original implementation to speed up tests | [here](../base-demo/src/test/java/software/xdev/tci/network/) | +| [Safe starting of named containers](./src/main/java/software/xdev/tci/safestart/)[JD](https://javadoc.io/doc/software.xdev.tci/base/latest/software/xdev/tci/safestart/SafeNamedContainerStarter.html) | Ensures that a container doesn't enter a crash loop during retried startups | [here](../base-demo/src/test/java/software/xdev/tci/safestart/) | | [Container Leak detection](./src/main/java/software/xdev/tci/leakdetection/)¹ | Prevents you from running out of resources | [here](../base-demo/src/test/java/software/xdev/tci/leak/) | | [Tracing](./src/main/java/software/xdev/tci/tracing/)¹ | Makes finding bottlenecks and similar problems easier | | +| [Reporting of fatal (Java) crashes during container startup](./src/main/java/software/xdev/tci/startup/error/java/fatal/) | Easier error diagnosis | [here](../advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/containers/WebAppContainer.java) | +| [Fail-fast/Abortable wait strategies](./src/main/java/software/xdev/tci/startup/wait/) | Fail fast when a container crashes - don't wait until the timeout | [here](../advanced-demo/integration-tests/tci-webapp/src/main/java/software/xdev/tci/demo/tci/webapp/containers/WebAppContainer.java) | ¹ = Active by default due to service loading diff --git a/base/src/main/java/software/xdev/tci/startup/error/java/fatal/HsErrPidStartUpCrashReporter.java b/base/src/main/java/software/xdev/tci/startup/error/java/fatal/HsErrPidStartUpCrashReporter.java index d48d96e3..89fca3c7 100644 --- a/base/src/main/java/software/xdev/tci/startup/error/java/fatal/HsErrPidStartUpCrashReporter.java +++ b/base/src/main/java/software/xdev/tci/startup/error/java/fatal/HsErrPidStartUpCrashReporter.java @@ -34,7 +34,7 @@ /** * Utility to log contents of java hs_err_pid files of a container that failed to start. *

- * Additionally also copies the file into target/ + * Additionally also copies the file into target/has_err_pid/<containerId>/<hs_err_pid-file> *

*/ public class HsErrPidStartUpCrashReporter From 39172ca2e081b009c65145b568b6a03fdfcb881f Mon Sep 17 00:00:00 2001 From: AB Date: Tue, 14 Jul 2026 14:38:03 +0200 Subject: [PATCH 116/141] Use constant instead of hardcoded string --- mockserver/pom.xml | 2 +- .../xdev/tci/mockserver/containers/TCIMockserverContainer.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mockserver/pom.xml b/mockserver/pom.xml index 2eec4022..c63e30e7 100644 --- a/mockserver/pom.xml +++ b/mockserver/pom.xml @@ -54,7 +54,7 @@ software.xdev.mockserver bom - 2.51.0 + 2.51.1 pom import diff --git a/mockserver/src/main/java/software/xdev/tci/mockserver/containers/TCIMockserverContainer.java b/mockserver/src/main/java/software/xdev/tci/mockserver/containers/TCIMockserverContainer.java index 50963957..44f0eaef 100644 --- a/mockserver/src/main/java/software/xdev/tci/mockserver/containers/TCIMockserverContainer.java +++ b/mockserver/src/main/java/software/xdev/tci/mockserver/containers/TCIMockserverContainer.java @@ -75,7 +75,7 @@ public static TCIMockserverContainer createDefaultForFactory() container .withCreateContainerCmdModifier(cmd -> cmd.getHostConfig().withMemory(ContainerMemory.M512M)) .waitingFor(new FastAbortOnContainerDeathWaitStrategy(new LogMessageWaitAbortableStrategy() - .withRegEx(".*started on port: " + PORT + ".*") + .withRegEx(MockServerContainer.LOG_MSG_WAIT_STRATEGY_REGEX) )); return container; } From 4334d355b133510bcf8d7cdaa0bc66ea5f28850e Mon Sep 17 00:00:00 2001 From: AB Date: Tue, 14 Jul 2026 15:02:29 +0200 Subject: [PATCH 117/141] Code cleanup --- .../HostPortWaitAbortableStrategy.java | 74 +++++++++++-------- 1 file changed, 43 insertions(+), 31 deletions(-) diff --git a/base/src/main/java/software/xdev/tci/startup/wait/strategy/HostPortWaitAbortableStrategy.java b/base/src/main/java/software/xdev/tci/startup/wait/strategy/HostPortWaitAbortableStrategy.java index 8a048f64..49b79e61 100644 --- a/base/src/main/java/software/xdev/tci/startup/wait/strategy/HostPortWaitAbortableStrategy.java +++ b/base/src/main/java/software/xdev/tci/startup/wait/strategy/HostPortWaitAbortableStrategy.java @@ -49,44 +49,27 @@ public class HostPortWaitAbortableStrategy extends AbstractWaitAbortableStrategy @Override protected void waitUntilReady(final AbortMonitor abortMonitor) { - final Set externalLivenessCheckPorts; - if(this.ports == null || this.ports.length == 0) + final Set externalLivenessCheckPorts = this.determineExternalLivenessCheckPorts(); + if(externalLivenessCheckPorts == null || externalLivenessCheckPorts.isEmpty()) { - externalLivenessCheckPorts = this.getLivenessCheckPorts(); - if(externalLivenessCheckPorts.isEmpty()) + if(LOG.isDebugEnabled()) { - if(LOG.isDebugEnabled()) - { - LOG.debug( - "Liveness check ports of {} is empty. Not waiting.", - this.waitStrategyTarget.getContainerInfo().getName() - ); - } - return; + LOG.debug( + "Liveness check ports of {} is empty. Not waiting.", + this.waitStrategyTarget.getContainerInfo().getName() + ); } - } - else - { - externalLivenessCheckPorts = - Arrays - .stream(this.ports) - .mapToObj(port -> this.waitStrategyTarget.getMappedPort(port)) - .collect(Collectors.toSet()); + return; } abortMonitor.throwIfRequired(); - final List exposedPorts = this.waitStrategyTarget.getExposedPorts(); - - final Set internalPorts = this.getInternalPorts(externalLivenessCheckPorts, exposedPorts); + final Set internalPorts = this.getInternalPorts( + externalLivenessCheckPorts, + this.waitStrategyTarget.getExposedPorts()); - final Callable internalCheck = - new InternalCommandPortListeningCheck(this.waitStrategyTarget, internalPorts); - - final Callable externalCheck = new ExternalPortListeningCheck( - this.waitStrategyTarget, - externalLivenessCheckPorts - ); + final Callable internalCheck = this.createInternalCheck(internalPorts); + final Callable externalCheck = this.createExternalCheck(externalLivenessCheckPorts); try { @@ -154,13 +137,42 @@ protected void waitUntilReady(final AbortMonitor abortMonitor) } } + private ExternalPortListeningCheck createExternalCheck(final Set externalLivenessCheckPorts) + { + return new ExternalPortListeningCheck( + this.waitStrategyTarget, + externalLivenessCheckPorts + ); + } + + private InternalCommandPortListeningCheck createInternalCheck(final Set internalPorts) + { + return new InternalCommandPortListeningCheck(this.waitStrategyTarget, internalPorts); + } + + /** + * @apiNote Can return null + */ + protected Set determineExternalLivenessCheckPorts() + { + if(this.ports == null || this.ports.length == 0) + { + return this.getLivenessCheckPorts(); + } + + return Arrays + .stream(this.ports) + .mapToObj(this.waitStrategyTarget::getMappedPort) + .collect(Collectors.toSet()); + } + protected Set getInternalPorts( final Set externalLivenessCheckPorts, final List exposedPorts) { return exposedPorts .stream() - .filter(it -> externalLivenessCheckPorts.contains(this.waitStrategyTarget.getMappedPort(it))) + .filter(port -> externalLivenessCheckPorts.contains(this.waitStrategyTarget.getMappedPort(port))) .collect(Collectors.toSet()); } From 0247e60e06dbb231796db2ffafa2262ae21d661a Mon Sep 17 00:00:00 2001 From: AB Date: Tue, 14 Jul 2026 15:16:14 +0200 Subject: [PATCH 118/141] Differ cache based on available AVX level See #688 --- .github/workflows/run-integration-tests.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run-integration-tests.yml b/.github/workflows/run-integration-tests.yml index ec93a1cf..edf8af06 100644 --- a/.github/workflows/run-integration-tests.yml +++ b/.github/workflows/run-integration-tests.yml @@ -151,12 +151,18 @@ jobs: - name: Test run: | + // Report CPU + cat /proc/cpuinfo | grep 'name' | uniq + + // Differentiate cache between AVX microcode used by Java (until https://github.com/xdev-software/tci/issues/688 is fixed) + java_useavx=$(java -XX:+PrintFlagsFinal -version 2> /dev/null | awk '/UseAVX/ {print $4}') + ./mvnw -B test \ -pl "advanced-demo/integration-tests/${{ matrix.project }}" -am \ -P run-it${{ matrix.jacoco && ',jacoco' || '' }} \ -Dtci.image-build.delete-on-exit=true \ - -Dtci.image-build.cache-from="type=gha,scope=§image${{ matrix.jacoco && '-jacoco' || '' }}" \ - -Dtci.image-build.cache-to="type=gha,mode=max,scope=§image${{ matrix.jacoco && '-jacoco' || '' }}" \ + -Dtci.image-build.cache-from="type=gha,scope=§image-j_avx${java_useavx}${{ matrix.jacoco && '-jacoco' || '' }}" \ + -Dtci.image-build.cache-to="type=gha,mode=max,scope=§image-j_avx${java_useavx}${{ matrix.jacoco && '-jacoco' || '' }}" \ ${{ matrix.pre-start && '-Dinfra-pre-start.enabled=1 ' || '' }} \ ${{ matrix.parallel > 0 && format('-Djunit.jupiter.execution.parallel.enabled=true -Djunit.jupiter.execution.parallel.mode.default=concurrent -Djunit.jupiter.execution.parallel.mode.classes.default=concurrent -Djunit.jupiter.execution.parallel.config.strategy=fixed -Djunit.jupiter.execution.parallel.config.fixed.parallelism=2 -Djunit.jupiter.execution.parallel.config.fixed.max-pool-size={0} ', matrix.parallel) || '' }} env: From 578dab83c8a01a9cebc0f6f10f7824e0dcfc683f Mon Sep 17 00:00:00 2001 From: AB Date: Tue, 14 Jul 2026 16:40:05 +0200 Subject: [PATCH 119/141] Make it possible to configure image build based on name of built image This is helpful when certain image builds require custom settings. Example: The dummy webapp requires custom images based on presence of AVX and jacoco --- .github/workflows/run-integration-tests.yml | 6 +- image-build/README.md | 12 +- .../xdev/tci/imagebuild/BuildImage.java | 4 +- ...er.java => BuildImageHandlerProvider.java} | 2 +- .../imagebuild/DefaultBuildImageHandler.java | 179 ------------------ .../DefaultBuildImageHandlerProvider.java | 67 +++++++ .../AbstractBuildImageHandlerConfig.java | 39 ++++ .../DefaultBuildImageHandlerConfig.java | 25 +-- .../OverlayBuildImageHandlerConfig.java | 77 ++++++++ .../handler/AbstractBuildImageHandler.java | 74 ++++++++ .../handler/AdvancedBuildImageHandler.java | 29 +++ .../imagebuild/handler/BuildImageHandler.java | 16 ++ .../NativeAdvancedBuildImageHandler.java | 107 +++++++++++ ...ware.xdev.tci.imagebuild.BuildImageHandler | 1 - ...v.tci.imagebuild.BuildImageHandlerProvider | 1 + 15 files changed, 433 insertions(+), 206 deletions(-) rename image-build/src/main/java/software/xdev/tci/imagebuild/{BuildImageHandler.java => BuildImageHandlerProvider.java} (96%) delete mode 100644 image-build/src/main/java/software/xdev/tci/imagebuild/DefaultBuildImageHandler.java create mode 100644 image-build/src/main/java/software/xdev/tci/imagebuild/DefaultBuildImageHandlerProvider.java create mode 100644 image-build/src/main/java/software/xdev/tci/imagebuild/config/AbstractBuildImageHandlerConfig.java create mode 100644 image-build/src/main/java/software/xdev/tci/imagebuild/config/OverlayBuildImageHandlerConfig.java create mode 100644 image-build/src/main/java/software/xdev/tci/imagebuild/handler/AbstractBuildImageHandler.java create mode 100644 image-build/src/main/java/software/xdev/tci/imagebuild/handler/AdvancedBuildImageHandler.java create mode 100644 image-build/src/main/java/software/xdev/tci/imagebuild/handler/BuildImageHandler.java create mode 100644 image-build/src/main/java/software/xdev/tci/imagebuild/handler/NativeAdvancedBuildImageHandler.java delete mode 100644 image-build/src/main/resources/META-INF/services/software.xdev.tci.imagebuild.BuildImageHandler create mode 100644 image-build/src/main/resources/META-INF/services/software.xdev.tci.imagebuild.BuildImageHandlerProvider diff --git a/.github/workflows/run-integration-tests.yml b/.github/workflows/run-integration-tests.yml index edf8af06..ca70a02d 100644 --- a/.github/workflows/run-integration-tests.yml +++ b/.github/workflows/run-integration-tests.yml @@ -161,8 +161,10 @@ jobs: -pl "advanced-demo/integration-tests/${{ matrix.project }}" -am \ -P run-it${{ matrix.jacoco && ',jacoco' || '' }} \ -Dtci.image-build.delete-on-exit=true \ - -Dtci.image-build.cache-from="type=gha,scope=§image-j_avx${java_useavx}${{ matrix.jacoco && '-jacoco' || '' }}" \ - -Dtci.image-build.cache-to="type=gha,mode=max,scope=§image-j_avx${java_useavx}${{ matrix.jacoco && '-jacoco' || '' }}" \ + -Dtci.image-build.cache-from="type=gha,scope=§image" \ + -Dtci.image-build.cache-to="type=gha,mode=max,scope=§image" \ + -Dtci.image-build.tci-demo-webapp.cache-from="type=gha,scope=§image-j_avx${java_useavx}${{ matrix.jacoco && '-jacoco' || '' }}" \ + -Dtci.image-build.tci-demo-webapp.cache-to="type=gha,mode=max,scope=§image-j_avx${java_useavx}${{ matrix.jacoco && '-jacoco' || '' }}" \ ${{ matrix.pre-start && '-Dinfra-pre-start.enabled=1 ' || '' }} \ ${{ matrix.parallel > 0 && format('-Djunit.jupiter.execution.parallel.enabled=true -Djunit.jupiter.execution.parallel.mode.default=concurrent -Djunit.jupiter.execution.parallel.mode.classes.default=concurrent -Djunit.jupiter.execution.parallel.config.strategy=fixed -Djunit.jupiter.execution.parallel.config.fixed.parallelism=2 -Djunit.jupiter.execution.parallel.config.fixed.max-pool-size={0} ', matrix.parallel) || '' }} env: diff --git a/image-build/README.md b/image-build/README.md index 12c4a9c2..ba834dd3 100644 --- a/image-build/README.md +++ b/image-build/README.md @@ -6,11 +6,17 @@ Provides common utility and configuration to build images.
The configuration is dynamically loaded from (sorted by highest priority) -* Environment variables - * prefixed with ``TCI_IMAGE-BUILD_`` +* Environment variables + * prefixed with `TCI_IMAGE-BUILD__` + * where `imageName` is the (sanitized) name of the image to build + * prefixed with `TCI_IMAGE-BUILD_` * all properties are in UPPERCASE and use `_` instead of `.` or `-` * System properties - * prefixed with ``tci.image-build.`` + * prefixed with `tci.image-build..` + * where `imageName` is the (sanitized) name of the image to build + * prefixed with `tci.image-build.` + +_NOTE: Sanitized image names only include alphanumeric characters, `_` or `-`. All other characters are replaced by `_`_
diff --git a/image-build/src/main/java/software/xdev/tci/imagebuild/BuildImage.java b/image-build/src/main/java/software/xdev/tci/imagebuild/BuildImage.java index eb385204..aa2c0017 100644 --- a/image-build/src/main/java/software/xdev/tci/imagebuild/BuildImage.java +++ b/image-build/src/main/java/software/xdev/tci/imagebuild/BuildImage.java @@ -41,9 +41,9 @@ public static String image( return impl().image(dockerImage, timeout, configure); } - public static BuildImageHandler impl() + public static BuildImageHandlerProvider impl() { - return TCIServiceLoaderHolder.instance().service(BuildImageHandler.class); + return TCIServiceLoaderHolder.instance().service(BuildImageHandlerProvider.class); } private BuildImage() diff --git a/image-build/src/main/java/software/xdev/tci/imagebuild/BuildImageHandler.java b/image-build/src/main/java/software/xdev/tci/imagebuild/BuildImageHandlerProvider.java similarity index 96% rename from image-build/src/main/java/software/xdev/tci/imagebuild/BuildImageHandler.java rename to image-build/src/main/java/software/xdev/tci/imagebuild/BuildImageHandlerProvider.java index cf08dc96..ed57af9e 100644 --- a/image-build/src/main/java/software/xdev/tci/imagebuild/BuildImageHandler.java +++ b/image-build/src/main/java/software/xdev/tci/imagebuild/BuildImageHandlerProvider.java @@ -22,7 +22,7 @@ import software.xdev.testcontainers.imagebuilder.buildxnative.NativeAdvancedImageFromDockerfile; -public interface BuildImageHandler +public interface BuildImageHandlerProvider { String nativeImage( String dockerImage, diff --git a/image-build/src/main/java/software/xdev/tci/imagebuild/DefaultBuildImageHandler.java b/image-build/src/main/java/software/xdev/tci/imagebuild/DefaultBuildImageHandler.java deleted file mode 100644 index 0a1360af..00000000 --- a/image-build/src/main/java/software/xdev/tci/imagebuild/DefaultBuildImageHandler.java +++ /dev/null @@ -1,179 +0,0 @@ -/* - * Copyright © 2025 XDEV Software (https://xdev.software) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package software.xdev.tci.imagebuild; - -import java.time.Duration; -import java.util.Optional; -import java.util.concurrent.CompletableFuture; -import java.util.function.UnaryOperator; -import java.util.regex.Pattern; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import software.xdev.tci.concurrent.TCIExecutorServiceHolder; -import software.xdev.tci.imagebuild.cache.async.TCICacheSaveRegistry; -import software.xdev.tci.imagebuild.config.BuildImageHandlerConfig; -import software.xdev.testcontainers.imagebuilder.AbstractImageFromDockerfile; -import software.xdev.testcontainers.imagebuilder.AdvancedImageFromDockerFile; -import software.xdev.testcontainers.imagebuilder.buildxnative.NativeAdvancedImageFromDockerfile; - - -public class DefaultBuildImageHandler implements BuildImageHandler -{ - private static final Logger LOG = LoggerFactory.getLogger(DefaultBuildImageHandler.class); - - protected static final Pattern DOCKER_IMAGE_SANITIZATION_PATTERN = - Pattern.compile("[^A-Za-z0-9-_]"); - - protected final BuildImageHandlerConfig config; - - public DefaultBuildImageHandler() - { - this(BuildImageHandlerConfig.instance()); - } - - public DefaultBuildImageHandler(final BuildImageHandlerConfig config) - { - this.config = config; - } - - @Override - public String nativeImage( - final String dockerImage, - final Duration timeout, - final UnaryOperator configure) - { - LOG.info("Starting build of (native) image {}", dockerImage); - - final String sanitizeDockerImageName = this.sanitizeDockerImageName(dockerImage); - final NativeAdvancedImageFromDockerfile builder = - new NativeAdvancedImageFromDockerfile(dockerImage, this.config.deleteOnExit()); - - this.configureAbstract(builder, sanitizeDockerImageName); - - this.templateCacheConfigValue(this.config.cacheFrom(), sanitizeDockerImageName) - .ifPresent(builder::withCacheFrom); - - if(this.config.saveCacheInBackground()) - { - builder.withCreateTransferFilesCache(true); - } - else - { - this.configureCacheTo(sanitizeDockerImageName, builder); - } - - final NativeAdvancedImageFromDockerfile customizedBuilder = configure.apply(builder); - - final String builtImageName = this.buildImage(customizedBuilder, timeout); - - if(this.config.saveCacheInBackground() && this.config.cacheTo().isPresent()) - { - LOG.info("Rebuilding image {} in background to save cache", builtImageName); - TCICacheSaveRegistry.instance().add( - CompletableFuture.runAsync( - () -> { - try - { - this.buildImage( - this.configureCacheTo( - sanitizeDockerImageName, - customizedBuilder.copyForExactRebuild(dockerImage + "-cache") - // Don't load it into docker because it's not needed there - .withLoad(false)), - timeout); - } - catch(final Exception ex) - { - LOG.warn("Rebuilding {} to save cache failed", builtImageName, ex); - } - finally - { - try - { - builder.cleanCreatedTransferFilesCache(); - } - catch(final Exception ex) - { - LOG.warn("Failed to clean created transfer file cache for {}", builtImageName, ex); - } - } - }, - TCIExecutorServiceHolder.instance())); - } - - return builtImageName; - } - - protected NativeAdvancedImageFromDockerfile configureCacheTo( - final String sanitizeDockerImageName, - final NativeAdvancedImageFromDockerfile builder) - { - this.templateCacheConfigValue(this.config.cacheTo(), sanitizeDockerImageName) - .ifPresent(builder::withCacheTo); - return builder; - } - - private Optional templateCacheConfigValue( - final Optional configValue, - final String sanitizeDockerImageName) - { - return configValue - .map(s -> s.replace("$image", sanitizeDockerImageName)) - .map(s -> s.replace("§image", sanitizeDockerImageName)); - } - - @Override - public String image( - final String dockerImage, - final Duration timeout, - final UnaryOperator configure) - { - LOG.info("Starting build of image {}", dockerImage); - - final String sanitizeDockerImageName = this.sanitizeDockerImageName(dockerImage); - final AdvancedImageFromDockerFile builder = - new AdvancedImageFromDockerFile(dockerImage, this.config.deleteOnExit()); - - this.configureAbstract(builder, sanitizeDockerImageName); - - return this.buildImage(configure.apply(builder), timeout); - } - - protected String buildImage( - final AbstractImageFromDockerfile builder, - final Duration timeout) - { - LOG.info("Building image {}...", builder.getDockerImageName()); - final String builtImageName = builder.build(timeout); - LOG.info("Built image {}", builtImageName); - - return builtImageName; - } - - protected > void configureAbstract( - final I builder, final String sanitizeDockerImageName) - { - builder.withLoggerForBuild( - LoggerFactory.getLogger(this.config.loggerForBuildPrefix() + sanitizeDockerImageName)); - } - - protected String sanitizeDockerImageName(final String dockerImage) - { - return DOCKER_IMAGE_SANITIZATION_PATTERN.matcher(dockerImage).replaceAll("_"); - } -} diff --git a/image-build/src/main/java/software/xdev/tci/imagebuild/DefaultBuildImageHandlerProvider.java b/image-build/src/main/java/software/xdev/tci/imagebuild/DefaultBuildImageHandlerProvider.java new file mode 100644 index 00000000..a2aa4df0 --- /dev/null +++ b/image-build/src/main/java/software/xdev/tci/imagebuild/DefaultBuildImageHandlerProvider.java @@ -0,0 +1,67 @@ +/* + * Copyright © 2025 XDEV Software (https://xdev.software) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package software.xdev.tci.imagebuild; + +import java.time.Duration; +import java.util.function.UnaryOperator; + +import software.xdev.tci.imagebuild.config.BuildImageHandlerConfig; +import software.xdev.tci.imagebuild.handler.AdvancedBuildImageHandler; +import software.xdev.tci.imagebuild.handler.NativeAdvancedBuildImageHandler; +import software.xdev.testcontainers.imagebuilder.AdvancedImageFromDockerFile; +import software.xdev.testcontainers.imagebuilder.buildxnative.NativeAdvancedImageFromDockerfile; + + +public class DefaultBuildImageHandlerProvider implements BuildImageHandlerProvider +{ + protected final BuildImageHandlerConfig config; + + public DefaultBuildImageHandlerProvider() + { + this(BuildImageHandlerConfig.instance()); + } + + public DefaultBuildImageHandlerProvider(final BuildImageHandlerConfig config) + { + this.config = config; + } + + @Override + public String nativeImage( + final String dockerImage, + final Duration timeout, + final UnaryOperator configure) + { + return new NativeAdvancedBuildImageHandler().build( + dockerImage, + this.config, + timeout, + configure); + } + + @Override + public String image( + final String dockerImage, + final Duration timeout, + final UnaryOperator configure) + { + return new AdvancedBuildImageHandler().build( + dockerImage, + this.config, + timeout, + configure); + } +} diff --git a/image-build/src/main/java/software/xdev/tci/imagebuild/config/AbstractBuildImageHandlerConfig.java b/image-build/src/main/java/software/xdev/tci/imagebuild/config/AbstractBuildImageHandlerConfig.java new file mode 100644 index 00000000..1f7ef873 --- /dev/null +++ b/image-build/src/main/java/software/xdev/tci/imagebuild/config/AbstractBuildImageHandlerConfig.java @@ -0,0 +1,39 @@ +/* + * Copyright © 2025 XDEV Software (https://xdev.software) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package software.xdev.tci.imagebuild.config; + +import java.util.Optional; + +import software.xdev.tci.config.DefaultConfig; + + +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") +public abstract class AbstractBuildImageHandlerConfig extends DefaultConfig implements BuildImageHandlerConfig +{ + protected static final String DELETE_ON_EXIT = "delete-on-exit"; + protected static final String LOGGER_FOR_BUILD_PREFIX = "logger-for-build-prefix"; + protected static final String CACHE_FROM = "cache-from"; + protected static final String CACHE_TO = "cache-to"; + protected static final String SAVE_CACHE_IN_BACKGROUND = "save-cache-in-background"; + protected static final String WAIT_FOR_SAVE_CACHE_IN_BACKGROUND = "wait-for-" + SAVE_CACHE_IN_BACKGROUND; + + protected Boolean deleteOnExit; + protected String loggerForBuildPrefix; + protected Optional cacheFrom; + protected Optional cacheTo; + protected Boolean saveCacheInBackground; + protected Boolean waitForSaveCacheInBackground; +} diff --git a/image-build/src/main/java/software/xdev/tci/imagebuild/config/DefaultBuildImageHandlerConfig.java b/image-build/src/main/java/software/xdev/tci/imagebuild/config/DefaultBuildImageHandlerConfig.java index 5d43b7ea..5cdf6bc9 100644 --- a/image-build/src/main/java/software/xdev/tci/imagebuild/config/DefaultBuildImageHandlerConfig.java +++ b/image-build/src/main/java/software/xdev/tci/imagebuild/config/DefaultBuildImageHandlerConfig.java @@ -17,21 +17,10 @@ import java.util.Optional; -import software.xdev.tci.config.DefaultConfig; - -@SuppressWarnings({"java:S2789", "OptionalAssignedToNull", "OptionalUsedAsFieldOrParameterType"}) -public class DefaultBuildImageHandlerConfig extends DefaultConfig implements BuildImageHandlerConfig +@SuppressWarnings({"java:S2789", "OptionalAssignedToNull"}) +public class DefaultBuildImageHandlerConfig extends AbstractBuildImageHandlerConfig { - protected static final String SAVE_CACHE_IN_BACKGROUND = "save-cache-in-background"; - - protected Boolean deleteOnExit; - protected String loggerForBuildPrefix; - protected Optional cacheFrom; - protected Optional cacheTo; - protected Boolean saveCacheInBackground; - protected Boolean waitForSaveCacheInBackground; - @Override protected String propertyNamePrefix() { @@ -43,7 +32,7 @@ public boolean deleteOnExit() { if(this.deleteOnExit == null) { - this.deleteOnExit = this.resolveBool("delete-on-exit", false); + this.deleteOnExit = this.resolveBool(DELETE_ON_EXIT, false); } return this.deleteOnExit; } @@ -53,7 +42,7 @@ public String loggerForBuildPrefix() { if(this.loggerForBuildPrefix == null) { - this.loggerForBuildPrefix = this.resolve("logger-for-build-prefix") + this.loggerForBuildPrefix = this.resolve(LOGGER_FOR_BUILD_PREFIX) .orElse("container.build."); } return this.loggerForBuildPrefix; @@ -64,7 +53,7 @@ public Optional cacheFrom() { if(this.cacheFrom == null) { - this.cacheFrom = this.resolve("cache-from"); + this.cacheFrom = this.resolve(CACHE_FROM); } return this.cacheFrom; } @@ -74,7 +63,7 @@ public Optional cacheTo() { if(this.cacheTo == null) { - this.cacheTo = this.resolve("cache-to"); + this.cacheTo = this.resolve(CACHE_TO); } return this.cacheTo; } @@ -96,7 +85,7 @@ public boolean waitForSaveCacheInBackground() if(this.waitForSaveCacheInBackground == null) { this.waitForSaveCacheInBackground = - this.resolveBool("wait-for-" + SAVE_CACHE_IN_BACKGROUND, true); + this.resolveBool(WAIT_FOR_SAVE_CACHE_IN_BACKGROUND, true); } return this.waitForSaveCacheInBackground; } diff --git a/image-build/src/main/java/software/xdev/tci/imagebuild/config/OverlayBuildImageHandlerConfig.java b/image-build/src/main/java/software/xdev/tci/imagebuild/config/OverlayBuildImageHandlerConfig.java new file mode 100644 index 00000000..c300456c --- /dev/null +++ b/image-build/src/main/java/software/xdev/tci/imagebuild/config/OverlayBuildImageHandlerConfig.java @@ -0,0 +1,77 @@ +package software.xdev.tci.imagebuild.config; + +import java.util.Optional; + + +@SuppressWarnings({"java:S2789", "OptionalAssignedToNull"}) +public class OverlayBuildImageHandlerConfig extends DefaultBuildImageHandlerConfig +{ + private final BuildImageHandlerConfig parentConfig; + private final String prefixName; + + public OverlayBuildImageHandlerConfig(final BuildImageHandlerConfig parentConfig, final String prefixName) + { + this.parentConfig = parentConfig; + this.prefixName = prefixName; + } + + @Override + protected String propertyNamePrefix() + { + return super.propertyNamePrefix() + "." + this.prefixName; + } + + @Override + public boolean deleteOnExit() + { + if(this.deleteOnExit == null) + { + this.deleteOnExit = this.resolveBool(DELETE_ON_EXIT, this.parentConfig.deleteOnExit()); + } + return this.deleteOnExit; + } + + @Override + public String loggerForBuildPrefix() + { + if(this.loggerForBuildPrefix == null) + { + this.loggerForBuildPrefix = this.resolve(LOGGER_FOR_BUILD_PREFIX) + .orElseGet(this.parentConfig::loggerForBuildPrefix); + } + return this.loggerForBuildPrefix; + } + + @Override + public Optional cacheFrom() + { + if(this.cacheFrom == null) + { + this.cacheFrom = this.resolve(CACHE_FROM) + .or(this.parentConfig::cacheFrom); + } + return this.cacheFrom; + } + + @Override + public Optional cacheTo() + { + if(this.cacheTo == null) + { + this.cacheTo = this.resolve(CACHE_TO) + .or(this.parentConfig::cacheTo); + } + return this.cacheTo; + } + + @Override + public boolean waitForSaveCacheInBackground() + { + if(this.waitForSaveCacheInBackground == null) + { + this.waitForSaveCacheInBackground = + this.resolveBool(WAIT_FOR_SAVE_CACHE_IN_BACKGROUND, this.parentConfig.waitForSaveCacheInBackground()); + } + return this.waitForSaveCacheInBackground; + } +} diff --git a/image-build/src/main/java/software/xdev/tci/imagebuild/handler/AbstractBuildImageHandler.java b/image-build/src/main/java/software/xdev/tci/imagebuild/handler/AbstractBuildImageHandler.java new file mode 100644 index 00000000..2e29fb26 --- /dev/null +++ b/image-build/src/main/java/software/xdev/tci/imagebuild/handler/AbstractBuildImageHandler.java @@ -0,0 +1,74 @@ +package software.xdev.tci.imagebuild.handler; + +import java.time.Duration; +import java.util.function.UnaryOperator; +import java.util.regex.Pattern; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import software.xdev.tci.imagebuild.config.BuildImageHandlerConfig; +import software.xdev.tci.imagebuild.config.OverlayBuildImageHandlerConfig; +import software.xdev.testcontainers.imagebuilder.AbstractImageFromDockerfile; + + +public abstract class AbstractBuildImageHandler> + implements BuildImageHandler +{ + protected static final Pattern DOCKER_IMAGE_SANITIZATION_PATTERN = Pattern.compile("[^A-Za-z0-9-_]"); + + protected Logger logger; + + protected AbstractBuildImageHandler() + { + this.logger = LoggerFactory.getLogger(this.getClass()); + } + + @Override + public String build( + final String dockerImage, + final BuildImageHandlerConfig parentConfig, + final Duration timeout, + final UnaryOperator configure) + { + final String sanitizedDockerImageName = this.sanitizeDockerImageName(dockerImage); + return this.build( + dockerImage, + sanitizedDockerImageName, + new OverlayBuildImageHandlerConfig(parentConfig, sanitizedDockerImageName), + timeout, + configure); + } + + protected abstract String build( + final String dockerImage, + final String sanitizedDockerImageName, + final BuildImageHandlerConfig parentConfig, + final Duration timeout, + final UnaryOperator configure); + + protected void configureAbstract( + final I builder, + final BuildImageHandlerConfig config, + final String sanitizeDockerImageName) + { + builder.withLoggerForBuild( + LoggerFactory.getLogger(config.loggerForBuildPrefix() + sanitizeDockerImageName)); + } + + protected String buildImage( + final AbstractImageFromDockerfile builder, + final Duration timeout) + { + this.logger.info("Building image {}...", builder.getDockerImageName()); + final String builtImageName = builder.build(timeout); + this.logger.info("Built image {}", builtImageName); + + return builtImageName; + } + + protected String sanitizeDockerImageName(final String dockerImage) + { + return DOCKER_IMAGE_SANITIZATION_PATTERN.matcher(dockerImage).replaceAll("_"); + } +} diff --git a/image-build/src/main/java/software/xdev/tci/imagebuild/handler/AdvancedBuildImageHandler.java b/image-build/src/main/java/software/xdev/tci/imagebuild/handler/AdvancedBuildImageHandler.java new file mode 100644 index 00000000..2c2d92c7 --- /dev/null +++ b/image-build/src/main/java/software/xdev/tci/imagebuild/handler/AdvancedBuildImageHandler.java @@ -0,0 +1,29 @@ +package software.xdev.tci.imagebuild.handler; + +import java.time.Duration; +import java.util.function.UnaryOperator; + +import software.xdev.tci.imagebuild.config.BuildImageHandlerConfig; +import software.xdev.testcontainers.imagebuilder.AdvancedImageFromDockerFile; + + +public class AdvancedBuildImageHandler extends AbstractBuildImageHandler +{ + @Override + protected String build( + final String dockerImage, + final String sanitizedDockerImageName, + final BuildImageHandlerConfig config, + final Duration timeout, + final UnaryOperator configure) + { + this.logger.info("Starting build of image {}", dockerImage); + + final AdvancedImageFromDockerFile builder = + new AdvancedImageFromDockerFile(dockerImage, config.deleteOnExit()); + + this.configureAbstract(builder, config, sanitizedDockerImageName); + + return this.buildImage(configure.apply(builder), timeout); + } +} diff --git a/image-build/src/main/java/software/xdev/tci/imagebuild/handler/BuildImageHandler.java b/image-build/src/main/java/software/xdev/tci/imagebuild/handler/BuildImageHandler.java new file mode 100644 index 00000000..c8bf53f1 --- /dev/null +++ b/image-build/src/main/java/software/xdev/tci/imagebuild/handler/BuildImageHandler.java @@ -0,0 +1,16 @@ +package software.xdev.tci.imagebuild.handler; + +import java.time.Duration; +import java.util.function.UnaryOperator; + +import software.xdev.tci.imagebuild.config.BuildImageHandlerConfig; + + +public interface BuildImageHandler +{ + String build( + String dockerImage, + BuildImageHandlerConfig parentConfig, + Duration timeout, + UnaryOperator configure); +} diff --git a/image-build/src/main/java/software/xdev/tci/imagebuild/handler/NativeAdvancedBuildImageHandler.java b/image-build/src/main/java/software/xdev/tci/imagebuild/handler/NativeAdvancedBuildImageHandler.java new file mode 100644 index 00000000..6a33a3c4 --- /dev/null +++ b/image-build/src/main/java/software/xdev/tci/imagebuild/handler/NativeAdvancedBuildImageHandler.java @@ -0,0 +1,107 @@ +package software.xdev.tci.imagebuild.handler; + +import java.time.Duration; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.function.UnaryOperator; + +import software.xdev.tci.concurrent.TCIExecutorServiceHolder; +import software.xdev.tci.imagebuild.cache.async.TCICacheSaveRegistry; +import software.xdev.tci.imagebuild.config.BuildImageHandlerConfig; +import software.xdev.testcontainers.imagebuilder.buildxnative.NativeAdvancedImageFromDockerfile; + + +public class NativeAdvancedBuildImageHandler extends AbstractBuildImageHandler +{ + @Override + protected String build( + final String dockerImage, + final String sanitizedDockerImageName, + final BuildImageHandlerConfig config, + final Duration timeout, + final UnaryOperator configure) + { + this.logger.info("Starting build of (native) image {}", dockerImage); + + final NativeAdvancedImageFromDockerfile builder = + new NativeAdvancedImageFromDockerfile(dockerImage, config.deleteOnExit()); + + this.configureAbstract(builder, config, sanitizedDockerImageName); + + this.templateCacheConfigValue(config.cacheFrom(), sanitizedDockerImageName) + .ifPresent(builder::withCacheFrom); + + if(config.saveCacheInBackground()) + { + builder.withCreateTransferFilesCache(true); + } + else + { + this.configureCacheTo(sanitizedDockerImageName, config, builder); + } + + final NativeAdvancedImageFromDockerfile customizedBuilder = configure.apply(builder); + + final String builtImageName = this.buildImage(customizedBuilder, timeout); + + if(config.saveCacheInBackground() && config.cacheTo().isPresent()) + { + this.logger.info("Rebuilding image {} in background to save cache", builtImageName); + TCICacheSaveRegistry.instance().add( + CompletableFuture.runAsync( + () -> { + try + { + this.buildImage( + this.configureCacheTo( + sanitizedDockerImageName, + config, + customizedBuilder.copyForExactRebuild(dockerImage + "-cache") + // Don't load it into docker because it's not needed there + .withLoad(false)), + timeout); + } + catch(final Exception ex) + { + this.logger.warn("Rebuilding {} to save cache failed", builtImageName, ex); + } + finally + { + try + { + builder.cleanCreatedTransferFilesCache(); + } + catch(final Exception ex) + { + this.logger.warn( + "Failed to clean created transfer file cache for {}", + builtImageName, + ex); + } + } + }, + TCIExecutorServiceHolder.instance())); + } + + return builtImageName; + } + + protected NativeAdvancedImageFromDockerfile configureCacheTo( + final String sanitizeDockerImageName, + final BuildImageHandlerConfig config, + final NativeAdvancedImageFromDockerfile builder) + { + this.templateCacheConfigValue(config.cacheTo(), sanitizeDockerImageName) + .ifPresent(builder::withCacheTo); + return builder; + } + + protected Optional templateCacheConfigValue( + final Optional configValue, + final String sanitizeDockerImageName) + { + return configValue + .map(s -> s.replace("$image", sanitizeDockerImageName)) + .map(s -> s.replace("§image", sanitizeDockerImageName)); + } +} diff --git a/image-build/src/main/resources/META-INF/services/software.xdev.tci.imagebuild.BuildImageHandler b/image-build/src/main/resources/META-INF/services/software.xdev.tci.imagebuild.BuildImageHandler deleted file mode 100644 index 0741e473..00000000 --- a/image-build/src/main/resources/META-INF/services/software.xdev.tci.imagebuild.BuildImageHandler +++ /dev/null @@ -1 +0,0 @@ -software.xdev.tci.imagebuild.DefaultBuildImageHandler diff --git a/image-build/src/main/resources/META-INF/services/software.xdev.tci.imagebuild.BuildImageHandlerProvider b/image-build/src/main/resources/META-INF/services/software.xdev.tci.imagebuild.BuildImageHandlerProvider new file mode 100644 index 00000000..bae2b157 --- /dev/null +++ b/image-build/src/main/resources/META-INF/services/software.xdev.tci.imagebuild.BuildImageHandlerProvider @@ -0,0 +1 @@ +software.xdev.tci.imagebuild.DefaultBuildImageHandlerProvider From e7ba0db941a61b50ac8a74b877f008b36306bdac Mon Sep 17 00:00:00 2001 From: AB Date: Tue, 14 Jul 2026 16:40:59 +0200 Subject: [PATCH 120/141] Java comments != Shell comments --- .github/workflows/run-integration-tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run-integration-tests.yml b/.github/workflows/run-integration-tests.yml index ca70a02d..228097ad 100644 --- a/.github/workflows/run-integration-tests.yml +++ b/.github/workflows/run-integration-tests.yml @@ -151,10 +151,10 @@ jobs: - name: Test run: | - // Report CPU + # Report CPU cat /proc/cpuinfo | grep 'name' | uniq - // Differentiate cache between AVX microcode used by Java (until https://github.com/xdev-software/tci/issues/688 is fixed) + # Differentiate cache between AVX microcode used by Java (until https://github.com/xdev-software/tci/issues/688 is fixed) java_useavx=$(java -XX:+PrintFlagsFinal -version 2> /dev/null | awk '/UseAVX/ {print $4}') ./mvnw -B test \ From b8e471534a48ea5970b4c801d0ff23ebaae5d5b1 Mon Sep 17 00:00:00 2001 From: AB Date: Tue, 14 Jul 2026 16:42:13 +0200 Subject: [PATCH 121/141] Add missing headers --- .../config/OverlayBuildImageHandlerConfig.java | 15 +++++++++++++++ .../handler/AbstractBuildImageHandler.java | 15 +++++++++++++++ .../handler/AdvancedBuildImageHandler.java | 15 +++++++++++++++ .../tci/imagebuild/handler/BuildImageHandler.java | 15 +++++++++++++++ .../handler/NativeAdvancedBuildImageHandler.java | 15 +++++++++++++++ 5 files changed, 75 insertions(+) diff --git a/image-build/src/main/java/software/xdev/tci/imagebuild/config/OverlayBuildImageHandlerConfig.java b/image-build/src/main/java/software/xdev/tci/imagebuild/config/OverlayBuildImageHandlerConfig.java index c300456c..65ffafdc 100644 --- a/image-build/src/main/java/software/xdev/tci/imagebuild/config/OverlayBuildImageHandlerConfig.java +++ b/image-build/src/main/java/software/xdev/tci/imagebuild/config/OverlayBuildImageHandlerConfig.java @@ -1,3 +1,18 @@ +/* + * Copyright © 2025 XDEV Software (https://xdev.software) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package software.xdev.tci.imagebuild.config; import java.util.Optional; diff --git a/image-build/src/main/java/software/xdev/tci/imagebuild/handler/AbstractBuildImageHandler.java b/image-build/src/main/java/software/xdev/tci/imagebuild/handler/AbstractBuildImageHandler.java index 2e29fb26..a2412174 100644 --- a/image-build/src/main/java/software/xdev/tci/imagebuild/handler/AbstractBuildImageHandler.java +++ b/image-build/src/main/java/software/xdev/tci/imagebuild/handler/AbstractBuildImageHandler.java @@ -1,3 +1,18 @@ +/* + * Copyright © 2025 XDEV Software (https://xdev.software) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package software.xdev.tci.imagebuild.handler; import java.time.Duration; diff --git a/image-build/src/main/java/software/xdev/tci/imagebuild/handler/AdvancedBuildImageHandler.java b/image-build/src/main/java/software/xdev/tci/imagebuild/handler/AdvancedBuildImageHandler.java index 2c2d92c7..cd013563 100644 --- a/image-build/src/main/java/software/xdev/tci/imagebuild/handler/AdvancedBuildImageHandler.java +++ b/image-build/src/main/java/software/xdev/tci/imagebuild/handler/AdvancedBuildImageHandler.java @@ -1,3 +1,18 @@ +/* + * Copyright © 2025 XDEV Software (https://xdev.software) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package software.xdev.tci.imagebuild.handler; import java.time.Duration; diff --git a/image-build/src/main/java/software/xdev/tci/imagebuild/handler/BuildImageHandler.java b/image-build/src/main/java/software/xdev/tci/imagebuild/handler/BuildImageHandler.java index c8bf53f1..399c9b82 100644 --- a/image-build/src/main/java/software/xdev/tci/imagebuild/handler/BuildImageHandler.java +++ b/image-build/src/main/java/software/xdev/tci/imagebuild/handler/BuildImageHandler.java @@ -1,3 +1,18 @@ +/* + * Copyright © 2025 XDEV Software (https://xdev.software) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package software.xdev.tci.imagebuild.handler; import java.time.Duration; diff --git a/image-build/src/main/java/software/xdev/tci/imagebuild/handler/NativeAdvancedBuildImageHandler.java b/image-build/src/main/java/software/xdev/tci/imagebuild/handler/NativeAdvancedBuildImageHandler.java index 6a33a3c4..112c9b8f 100644 --- a/image-build/src/main/java/software/xdev/tci/imagebuild/handler/NativeAdvancedBuildImageHandler.java +++ b/image-build/src/main/java/software/xdev/tci/imagebuild/handler/NativeAdvancedBuildImageHandler.java @@ -1,3 +1,18 @@ +/* + * Copyright © 2025 XDEV Software (https://xdev.software) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package software.xdev.tci.imagebuild.handler; import java.time.Duration; From d818db8abaa1de9684000f0bcbecbddbb4adda4e Mon Sep 17 00:00:00 2001 From: AB Date: Tue, 14 Jul 2026 16:44:35 +0200 Subject: [PATCH 122/141] Update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 28e2023b..56e02952 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ * Added `image-build` module * This module utilizes v4 of [testcontainers-advanced-imagebuilder](https://github.com/xdev-software/testcontainers-advanced-imagebuilder) * It contains some shortcuts that help with common configuration e.g. related to caching (see below) + * Can be configured using environment variables or system properties (see corresponding configuration section for details) * Added dedicated composite actions for caching * These can be found in `.github/actions/docker-image-cache` * Currently 3 actions exist: @@ -12,6 +13,7 @@ * primarily designed to be used with `FastAbortOnContainerDeathWaitStrategy` * the strategies can be aborted e.g. when a container died during startup * a dead container therefore no longer has to wait until the corresponding strategy times out + * Implemented in all applicable modules * `db-jdbc-spring-orm` & implementations * No longer use `setPersistenceProviderClassName` because it has no effect * Performance: Reuse `PersistenceProvider` From ebdb60b0a3f799e83803fc1850f685f470bd09a7 Mon Sep 17 00:00:00 2001 From: "Alex \"Blex\" B" <45384811+AB-xdev@users.noreply.github.com> Date: Tue, 14 Jul 2026 17:01:06 +0200 Subject: [PATCH 123/141] Update CONTRIBUTING.md Update recommended setup --- CONTRIBUTING.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2c7b0250..c77eab35 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,14 +23,15 @@ You should have the following things installed: * Maven (Note that the [Maven Wrapper](https://maven.apache.org/wrapper/) is shipped with the repo) ### Recommended setup -* Install ``IntelliJ`` (Community Edition is sufficient) - * Install the following plugins: - * [Save Actions](https://plugins.jetbrains.com/plugin/22113) - Provides save actions, like running the formatter or adding ``final`` to fields - * [SonarLint](https://plugins.jetbrains.com/plugin/7973-sonarlint) - CodeStyle/CodeAnalysis - * You may consider disabling telemetry in the settings under ``Tools > Sonarlint -> About`` - * [Checkstyle-IDEA](https://plugins.jetbrains.com/plugin/1065-checkstyle-idea) - CodeStyle/CodeAnalysis +* Install `IntelliJ` + * Recommended setup actions + * Disable not needed plugins + * Disable [telemetry](https://www.jetbrains.com/help/idea/settings-usage-statistics.html) + * Consider [disabling the integrated webserver for security reasons](https://intellij-support.jetbrains.com/hc/en-us/community/posts/8125278285586-Internal-Local-ports-63342-6942?page=1#community_comment_8141506876562) using `-Didea.builtin.server.disabled=true` + * Configure the available memory * Import the project - * Ensure that everything is encoded in ``UTF-8`` + * You will get prompted to install the required plugins + * Ensure that everything is encoded in `UTF-8` * Ensure that the JDK/Java-Version is correct From 4dc1db8168698f69b147261da2ee21a478a190f4 Mon Sep 17 00:00:00 2001 From: "Alex \"Blex\" B" <45384811+AB-xdev@users.noreply.github.com> Date: Tue, 14 Jul 2026 17:18:14 +0200 Subject: [PATCH 124/141] Update CONTRIBUTING.md This options crashes the build --- CONTRIBUTING.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c77eab35..6e9dde56 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -27,7 +27,6 @@ You should have the following things installed: * Recommended setup actions * Disable not needed plugins * Disable [telemetry](https://www.jetbrains.com/help/idea/settings-usage-statistics.html) - * Consider [disabling the integrated webserver for security reasons](https://intellij-support.jetbrains.com/hc/en-us/community/posts/8125278285586-Internal-Local-ports-63342-6942?page=1#community_comment_8141506876562) using `-Didea.builtin.server.disabled=true` * Configure the available memory * Import the project * You will get prompted to install the required plugins From dd842234927835ebb0342e0b61d5c0d26d41e175 Mon Sep 17 00:00:00 2001 From: AB Date: Wed, 15 Jul 2026 08:58:36 +0200 Subject: [PATCH 125/141] Update and standardize browser config --- CHANGELOG.md | 1 + .../xdev/tci/config/DefaultConfig.java | 9 +- selenium/README.md | 26 ++++++ .../xdev/tci/selenium/BrowserTCI.java | 2 +- .../selenium/factory/BrowserTCIFactory.java | 4 +- .../config/BrowserTCIFactoryConfig.java | 2 +- .../DefaultBrowserTCIFactoryConfig.java | 87 ++++++++++++++++--- 7 files changed, 113 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 56e02952..142feea7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ * Improve container cleanup * Updated dependencies * Updated demo +* Updated docs # 3.4.1 * Remove dedicated code for Java 17 (no longer needed because Java 21 is the minimum) diff --git a/base/src/main/java/software/xdev/tci/config/DefaultConfig.java b/base/src/main/java/software/xdev/tci/config/DefaultConfig.java index e8d3dcd0..4c5d06e9 100644 --- a/base/src/main/java/software/xdev/tci/config/DefaultConfig.java +++ b/base/src/main/java/software/xdev/tci/config/DefaultConfig.java @@ -36,11 +36,16 @@ protected Optional resolve(final String propertyName) protected boolean resolveBool(final String propertyName, final boolean defaultVal) { - return this.resolve(propertyName) - .map(s -> "1".equals(s) || Boolean.parseBoolean(s)) + return this.resolveBool(propertyName) .orElse(defaultVal); } + protected Optional resolveBool(final String propertyName) + { + return this.resolve(propertyName) + .map(s -> "1".equals(s) || Boolean.parseBoolean(s)); + } + protected int resolveInt(final String propertyName, final int defaultVal) { return this.resolveInt(propertyName, () -> defaultVal); diff --git a/selenium/README.md b/selenium/README.md index 75811157..de4650c8 100644 --- a/selenium/README.md +++ b/selenium/README.md @@ -9,3 +9,29 @@ TCI for [Selenium](https://github.com/SeleniumHQ/selenium). * NoVNC support * Full scale support for recording videos * Browser Logs can be enabled if required + +## Config + +
The configuration is dynamically loaded from (sorted by highest priority) + +* Environment variables + * prefixed with ``TCI_SELENIUM_`` + * all properties are in UPPERCASE and use `_` instead of `.` or `-` +* System properties + * prefixed with ``tci.selenium.`` + +
+ +
Full list of configuration options + +_Please note that the preconfigured values usually work out of the box.
_ +_You should know exactly what you're doing when doing modifications._ + +| Property | Type | Default | Notes | +| --- | --- | --- | --- | +| `record-mode` | `Enum` | `RECORD_FAILING` | Recording mode.
Available:
  • SKIP - Do not record any videos
  • RECORD_ALL - Record all tests
  • RECORD_FAILING - Record failing tests only
| +| `dir-for-records` | `String` | `target/records` | Directory for storing the recorded videos | +| `vnc-enabled` | `bool` | `false` | Enable [VNC](https://en.wikipedia.org/wiki/VNC) and [NoVNC](https://github.com/novnc/novnc). This is usually only needed during debugging. | +| `bidi-enabled` | `bool` | `true` | Use [Selenium BiDirectional functionality](https://www.selenium.dev/documentation/webdriver/bidi/) instead of legacy Chrome DevTools Protocol (CDP).
Disabling this will make certain operations unavailable e.g. listening for browser logs. | +| `deactivate-cdp-if-possible` | `bool` | `true` | Disable Chrome DevTools Protocol (CDP) if possible.
CDP requires additional maven dependencies (e.g. `selenium-devtools-v137)` that are not present by default and will result in a warning. | +| `min-browser-console-log-level` | `Enum` | `ERROR` | Prints out browser console logs. Configures the MINIMUM log level.
Available options:
  • OFF
  • ERROR
  • WARN
  • INFO
  • DEBUG
  • ALL
diff --git a/selenium/src/main/java/software/xdev/tci/selenium/BrowserTCI.java b/selenium/src/main/java/software/xdev/tci/selenium/BrowserTCI.java index c88cdc11..7009160d 100644 --- a/selenium/src/main/java/software/xdev/tci/selenium/BrowserTCI.java +++ b/selenium/src/main/java/software/xdev/tci/selenium/BrowserTCI.java @@ -62,7 +62,7 @@ public class BrowserTCI extends TCI // https://www.selenium.dev/documentation/webdriver/bidi protected boolean bidiEnabled = true; - // Disables the (not standardized) Chrome Dev Tools (CDP) protocol (when bidi is enabled). + // Disables the (not standardized) Chrome DevTools protocol (CDP) when bidi is enabled. // CDP requires additional maven dependencies (e.g. selenium-devtools-v137) that are // NOT present and result in a warning. protected boolean deactivateCDPIfPossible = true; diff --git a/selenium/src/main/java/software/xdev/tci/selenium/factory/BrowserTCIFactory.java b/selenium/src/main/java/software/xdev/tci/selenium/factory/BrowserTCIFactory.java index 65235342..f153a52d 100644 --- a/selenium/src/main/java/software/xdev/tci/selenium/factory/BrowserTCIFactory.java +++ b/selenium/src/main/java/software/xdev/tci/selenium/factory/BrowserTCIFactory.java @@ -88,8 +88,8 @@ public static BrowserTCI createDefaultBrowserTCI( .withWebDriverRetryCount(Math.max(Math.min(cpuSlownessFactor(), 5), 1)) .withWebDriverRetrySec(25 + cpuSlownessFactor() * 5) .withBrowserConsoleLog( - logBrowserConsoleConsumer(config.browserConsoleLogLevel()), - config.browserConsoleLogLevel().logLevels()); + logBrowserConsoleConsumer(config.minBrowserConsoleLogLevel()), + config.minBrowserConsoleLogLevel().logLevels()); } @SuppressWarnings({"resource", "checkstyle:MagicNumber"}) diff --git a/selenium/src/main/java/software/xdev/tci/selenium/factory/config/BrowserTCIFactoryConfig.java b/selenium/src/main/java/software/xdev/tci/selenium/factory/config/BrowserTCIFactoryConfig.java index f83f64f0..883b7d77 100644 --- a/selenium/src/main/java/software/xdev/tci/selenium/factory/config/BrowserTCIFactoryConfig.java +++ b/selenium/src/main/java/software/xdev/tci/selenium/factory/config/BrowserTCIFactoryConfig.java @@ -33,5 +33,5 @@ public interface BrowserTCIFactoryConfig boolean deactivateCdpIfPossible(); - BrowserTCIFactory.BrowserConsoleLogLevel browserConsoleLogLevel(); + BrowserTCIFactory.BrowserConsoleLogLevel minBrowserConsoleLogLevel(); } diff --git a/selenium/src/main/java/software/xdev/tci/selenium/factory/config/DefaultBrowserTCIFactoryConfig.java b/selenium/src/main/java/software/xdev/tci/selenium/factory/config/DefaultBrowserTCIFactoryConfig.java index fa1ac71e..22119602 100644 --- a/selenium/src/main/java/software/xdev/tci/selenium/factory/config/DefaultBrowserTCIFactoryConfig.java +++ b/selenium/src/main/java/software/xdev/tci/selenium/factory/config/DefaultBrowserTCIFactoryConfig.java @@ -30,12 +30,43 @@ public class DefaultBrowserTCIFactoryConfig extends DefaultConfig implements Bro { private static final Logger LOG = LoggerFactory.getLogger(DefaultBrowserTCIFactoryConfig.class); - public static final String RECORD_MODE = "recordMode"; - public static final String RECORD_DIR = "recordDir"; - public static final String VNC_ENABLED = "vncEnabled"; - public static final String BIDI_ENABLED = "bidiEnabled"; - public static final String DEACTIVATE_CDP_IF_POSSIBLE = "deactivateCdpIfPossible"; - public static final String BROWSER_CONSOLE_LOG_LEVEL = "browserConsoleLogLevel"; + /** + * @deprecated Use non legacy option instead + */ + @Deprecated(since = "4.0.0") + public static final String LEGACY_RECORD_MODE = "recordMode"; + /** + * @deprecated Use non legacy option instead + */ + @Deprecated(since = "4.0.0") + public static final String LEGACY_RECORD_DIR = "recordDir"; + /** + * @deprecated Use non legacy option instead + */ + @Deprecated(since = "4.0.0") + public static final String LEGACY_VNC_ENABLED = "vncEnabled"; + /** + * @deprecated Use non legacy option instead + */ + @Deprecated(since = "4.0.0") + public static final String LEGACY_BIDI_ENABLED = "bidiEnabled"; + /** + * @deprecated Use non legacy option instead + */ + @Deprecated(since = "4.0.0") + public static final String LEGACY_DEACTIVATE_CDP_IF_POSSIBLE = "deactivateCdpIfPossible"; + /** + * @deprecated Use non legacy option instead + */ + @Deprecated(since = "4.0.0") + public static final String LEGACY_BROWSER_CONSOLE_LOG_LEVEL = "browserConsoleLogLevel"; + + public static final String RECORD_MODE = "record-mode"; + public static final String RECORD_DIR = "record-dir"; + public static final String VNC_ENABLED = "vnc-enabled"; + public static final String BIDI_ENABLED = "bidi-enabled"; + public static final String DEACTIVATE_CDP_IF_POSSIBLE = "deactivate-cdp-if-possible"; + public static final String MIN_BROWSER_CONSOLE_LOG_LEVEL = "min-browser-console-log-level"; public static final String DEFAULT_RECORD_DIR = "target/records"; @@ -57,7 +88,10 @@ public BrowserWebDriverContainer.RecordingMode recordingMode() { if(this.systemRecordingMode == null) { - final String resolvedRecordMode = this.resolve(RECORD_MODE).orElse(null); + final String resolvedRecordMode = this.resolve(RECORD_MODE) + .or(() -> this.resolve(LEGACY_RECORD_MODE) + .map(v -> this.reportLegacyConfigOption(LEGACY_RECORD_MODE, RECORD_MODE, v))) + .orElse(null); this.systemRecordingMode = Stream.of(BrowserWebDriverContainer.RecordingMode.values()) .filter(rm -> rm.toString().equals(resolvedRecordMode)) .findFirst() @@ -72,7 +106,10 @@ public Path dirForRecords() { if(this.dirForRecords == null) { - this.dirForRecords = Path.of(this.resolve(RECORD_DIR).orElse(DEFAULT_RECORD_DIR)); + this.dirForRecords = Path.of(this.resolve(RECORD_DIR) + .or(() -> this.resolve(LEGACY_RECORD_DIR) + .map(v -> this.reportLegacyConfigOption(LEGACY_RECORD_DIR, RECORD_DIR, v))) + .orElse(DEFAULT_RECORD_DIR)); final boolean wasCreated = this.dirForRecords.toFile().mkdirs(); LOG.info( "Default directory for records='{}', created={}", this.dirForRecords.toAbsolutePath(), @@ -87,7 +124,10 @@ public boolean vncEnabled() { if(this.vncEnabled == null) { - this.vncEnabled = this.resolveBool(VNC_ENABLED, false); + this.vncEnabled = this.resolveBool(VNC_ENABLED) + .or(() -> this.resolveBool(LEGACY_VNC_ENABLED) + .map(v -> this.reportLegacyConfigOption(LEGACY_VNC_ENABLED, VNC_ENABLED, v))) + .orElse(false); LOG.info("VNC enabled={}", this.vncEnabled); } return this.vncEnabled; @@ -98,7 +138,10 @@ public boolean bidiEnabled() { if(this.bidiEnabled == null) { - this.bidiEnabled = this.resolveBool(BIDI_ENABLED, true); + this.bidiEnabled = this.resolveBool(BIDI_ENABLED) + .or(() -> this.resolveBool(LEGACY_BIDI_ENABLED) + .map(v -> this.reportLegacyConfigOption(LEGACY_BIDI_ENABLED, BIDI_ENABLED, v))) + .orElse(true); LOG.info("BiDi enabled={}", this.bidiEnabled); } return this.bidiEnabled; @@ -116,15 +159,35 @@ public boolean deactivateCdpIfPossible() } @Override - public BrowserTCIFactory.BrowserConsoleLogLevel browserConsoleLogLevel() + public BrowserTCIFactory.BrowserConsoleLogLevel minBrowserConsoleLogLevel() { if(this.browserConsoleLogLevel == null) { - this.browserConsoleLogLevel = this.resolve(BROWSER_CONSOLE_LOG_LEVEL) + this.browserConsoleLogLevel = this.resolve(MIN_BROWSER_CONSOLE_LOG_LEVEL) + .or(() -> this.resolve(LEGACY_BROWSER_CONSOLE_LOG_LEVEL) + .map(v -> this.reportLegacyConfigOption( + LEGACY_BROWSER_CONSOLE_LOG_LEVEL, + MIN_BROWSER_CONSOLE_LOG_LEVEL, + v)) + ) .map(BrowserTCIFactory.BrowserConsoleLogLevel::valueOf) .orElse(BrowserTCIFactory.BrowserConsoleLogLevel.ERROR); LOG.info("BrowserConsoleLogLevel={}", this.browserConsoleLogLevel); } return this.browserConsoleLogLevel; } + + protected T reportLegacyConfigOption(final String legacy, final String upToDate, final T value) + { + this.reportLegacyConfigOption(legacy, upToDate); + return value; + } + + protected void reportLegacyConfigOption(final String legacy, final String upToDate) + { + LOG.warn( + "Detected deprecated config option that will be removed: {} - use {} instead", + legacy, + upToDate); + } } From 89e9a4b03e893be0d9bb75f80b65913c5da97314 Mon Sep 17 00:00:00 2001 From: AB Date: Wed, 15 Jul 2026 09:36:19 +0200 Subject: [PATCH 126/141] Standardize and update config properties Fixes #562 --- CHANGELOG.md | 10 ++++ base/README.md | 56 +++++++++++++++++++ .../xdev/tci/config/DefaultConfig.java | 22 +++++++- .../prestart/PreStartableTCIFactory.java | 2 +- .../config/DefaultPreStartConfig.java | 15 ++++- .../config/DefaultLeakDetectionConfig.java | 2 +- image-build/README.md | 5 +- jacoco/README.md | 7 +-- selenium/README.md | 9 ++- .../DefaultBrowserTCIFactoryConfig.java | 14 ----- 10 files changed, 110 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 142feea7..dbe6104f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,16 @@ * `db-jdbc-spring-orm` & implementations * No longer use `setPersistenceProviderClassName` because it has no effect * Performance: Reuse `PersistenceProvider` +* Standardized some configuration properties + * `tci.selenium` + * `recordMode` → `record-mode` + * `recordDir` → `record-dir` + * `vncEnabled` → `vnc-enabled` + * `bidiEnabled` → `bidi-enabled` + * `deactivateCdpIfPossible` → `deactivate-cdp-if-possible` + * `browserConsoleLogLevel` → `min-browser-console-log-level` + * `infra-pre-start` → `tci.infra-pre-start` + * `leak-detection` → `tci.leak-detection` * Fixed a deadlock during service resolution * Fixed rate limiting in some places * Improve container cleanup diff --git a/base/README.md b/base/README.md index 78ad3563..bc62c4c0 100644 --- a/base/README.md +++ b/base/README.md @@ -20,3 +20,59 @@ Base for other modules and core components. ## Usage Take a look at the [minimalistic demo](../base-demo/) that showcases the components individually. + +## Config + +### PreStart + +
The configuration is dynamically loaded from (sorted by highest priority) + +* Environment variables + * prefixed with `TCI_INFRA-PRE-START__`* + * prefixed with `TCI_INFRA-PRE-START_` + * all properties are in UPPERCASE and use `_` instead of `.` or `-` +* System properties + * prefixed with `tci.infra-pre-start..`* + * prefixed with `tci.infra-pre-start.` + +_NOTE: `*` indicates that only some properties support this_ + +
+ +
Full list of configuration options + +| Property | Type | Default | Notes | +| --- | --- | --- | --- | +| `enabled` | `bool` | `false` | Should PreStarting be enabled? | +| `keep-ready`* | `int` | [`junit.jupiter.execution.parallel.config.fixed.max-pool-size`](https://docs.junit.org/6.1.2/writing-tests/parallel-execution.html) or `1` | How many container should be kept ready for use in the background?
Setting this to a value `< 0` will effectively disable PreStarting | +| `max-start-simultan`* | `int` | [`junit.jupiter.execution.parallel.config.fixed.max-pool-size`](https://docs.junit.org/6.1.2/writing-tests/parallel-execution.html) or `1` | How many contains should be started simultaneously?
Setting a negative value will remove this limitation | +| `direct-network-attach-if-possible`* | `bool` | `true` |
  • true - Directly attaches the container to the network during startup if possible
  • false - Always performs a network-connect as if PreStarting is active. This is slower, however it emulates PreStarting better and may help with finding bugs.
| +| `fixate-exposed-ports-if-required`* | `bool` | `true` | Fixates exposed ports when no direct network attach is possible. This is a workaround for moby/moby#44137. | +| `coordinator.idle-cpu-percent` | `int` | `40`% | Amount of CPU that needs to be idle to allow PreStarting of containers | +| `coordinator.schedule-period-ms` | `int` | `1000` (1s) | How often PreStarting (one factory) should be tried | +| `detect-ending-tests` | `bool` | `true` | Should PreStarting be stopped when tests are ending? | + +_NOTE: Properties marked with `*` can additionally can use the `preStartName` for configuration. Example: `tci.infra-pre-start.my-webapp.`_ + +
+ +### Leak Detection + +
The configuration is dynamically loaded from (sorted by highest priority) + +* Environment variables + * prefixed with `TCI_LEAK-DETECTION_` + * all properties are in UPPERCASE and use `_` instead of `.` or `-` +* System properties + * prefixed with `tci.leak-detection.` + +
+ +
Full list of configuration options + +| Property | Type | Default | Notes | +| --- | --- | --- | --- | +| `enabled` | `bool` | `false` | Should Leak-Detection be enabled? | +| `stop-timeout-ms` | `int` | ~`20000` (~20s) actual value depends on cpuSlownessFactor | How long to wait until all infrastructure is stopped after tests have ended | + +
diff --git a/base/src/main/java/software/xdev/tci/config/DefaultConfig.java b/base/src/main/java/software/xdev/tci/config/DefaultConfig.java index 4c5d06e9..38cf75d7 100644 --- a/base/src/main/java/software/xdev/tci/config/DefaultConfig.java +++ b/base/src/main/java/software/xdev/tci/config/DefaultConfig.java @@ -20,6 +20,8 @@ import java.util.function.IntSupplier; import java.util.function.LongSupplier; +import org.slf4j.LoggerFactory; + public abstract class DefaultConfig { @@ -27,7 +29,11 @@ public abstract class DefaultConfig protected Optional resolve(final String propertyName) { - final String fullPropertyName = this.propertyNamePrefix() + "." + propertyName; + return this.resolveFullyBuildPropertyName(this.propertyNamePrefix() + "." + propertyName); + } + + protected Optional resolveFullyBuildPropertyName(final String fullPropertyName) + { return Optional.ofNullable(System.getenv(fullPropertyName .replace(".", "_") .toUpperCase(Locale.ROOT))) @@ -82,4 +88,18 @@ protected long resolveLong(final String propertyName, final LongSupplier default }) .orElseGet(defaultValueSupplier::getAsLong); } + + protected T reportLegacyConfigOption(final String legacy, final String upToDate, final T value) + { + this.reportLegacyConfigOption(legacy, upToDate); + return value; + } + + protected void reportLegacyConfigOption(final String legacy, final String upToDate) + { + LoggerFactory.getLogger(this.getClass()).warn( + "Detected deprecated config option that will be removed: {} - use {} instead", + legacy, + upToDate); + } } diff --git a/base/src/main/java/software/xdev/tci/factory/prestart/PreStartableTCIFactory.java b/base/src/main/java/software/xdev/tci/factory/prestart/PreStartableTCIFactory.java index ea2bc211..0b1ed959 100644 --- a/base/src/main/java/software/xdev/tci/factory/prestart/PreStartableTCIFactory.java +++ b/base/src/main/java/software/xdev/tci/factory/prestart/PreStartableTCIFactory.java @@ -150,7 +150,7 @@ public class PreStartableTCIFactory, I extends TCI /** * Has the following effects: *
    - *
  • true (default) - Directly attaches the Container to the network during startup if + *
  • true (default) - Directly attaches the container to the network during startup if * possible
  • *
  • false - Performs a Network#connect as if PreStarting is active. * This is slower however it emulates PreStarting better and may help finding bugs.
  • diff --git a/base/src/main/java/software/xdev/tci/factory/prestart/config/DefaultPreStartConfig.java b/base/src/main/java/software/xdev/tci/factory/prestart/config/DefaultPreStartConfig.java index 76f45e6e..7ffffbf6 100644 --- a/base/src/main/java/software/xdev/tci/factory/prestart/config/DefaultPreStartConfig.java +++ b/base/src/main/java/software/xdev/tci/factory/prestart/config/DefaultPreStartConfig.java @@ -90,7 +90,20 @@ public DefaultPreStartConfig() @Override public String propertyNamePrefix() { - return "infra-pre-start"; + return "tci.infra-pre-start"; + } + + @Override + protected Optional resolve(final String propertyName) + { + return super.resolve(propertyName) + .or(() -> this.resolveFullyBuildPropertyName("infra-prestart." + propertyName) + .map(v -> this.reportLegacyConfigOption( + "infra-prestart." + propertyName, + this.propertyNamePrefix() + "." + propertyName, + v) + ) + ); } @Override diff --git a/base/src/main/java/software/xdev/tci/leakdetection/config/DefaultLeakDetectionConfig.java b/base/src/main/java/software/xdev/tci/leakdetection/config/DefaultLeakDetectionConfig.java index a6664d55..c827642f 100644 --- a/base/src/main/java/software/xdev/tci/leakdetection/config/DefaultLeakDetectionConfig.java +++ b/base/src/main/java/software/xdev/tci/leakdetection/config/DefaultLeakDetectionConfig.java @@ -35,7 +35,7 @@ public DefaultLeakDetectionConfig() @Override protected String propertyNamePrefix() { - return "leak-detection"; + return "tci.leak-detection"; } @Override diff --git a/image-build/README.md b/image-build/README.md index ba834dd3..2430e99d 100644 --- a/image-build/README.md +++ b/image-build/README.md @@ -16,15 +16,12 @@ Provides common utility and configuration to build images. * where `imageName` is the (sanitized) name of the image to build * prefixed with `tci.image-build.` -_NOTE: Sanitized image names only include alphanumeric characters, `_` or `-`. All other characters are replaced by `_`_ +_NOTE: Sanitized image-names only include alphanumeric characters, `_` or `-`. All other characters are replaced by `_`_
Full list of configuration options -_Please note that the preconfigured values usually work out of the box.
_ -_You should know exactly what you're doing when doing modifications._ - | Property | Type | Default | Notes | | --- | --- | --- | --- | | `delete-on-exit` | `bool` | `false` | Should the image be deleted on exit? | diff --git a/jacoco/README.md b/jacoco/README.md index def6f75d..c746e030 100644 --- a/jacoco/README.md +++ b/jacoco/README.md @@ -12,18 +12,15 @@ Please have a look at the [advanced-demo](../advanced-demo/) and the correspondi
The configuration is dynamically loaded from (sorted by highest priority) * Environment variables - * prefixed with ``TCI_JACOCO_`` + * prefixed with `TCI_JACOCO_` * all properties are in UPPERCASE and use `_` instead of `.` or `-` * System properties - * prefixed with ``tci.jacoco.`` + * prefixed with `tci.jacoco.`
Full list of configuration options -_Please note that the preconfigured values usually work out of the box.
_ -_You should know exactly what you're doing when doing modifications._ - | Property | Type | Default | Notes | | --- | --- | --- | --- | | `enabled` | `bool` | `false` | You should probably set this to `true` | diff --git a/selenium/README.md b/selenium/README.md index de4650c8..274838d8 100644 --- a/selenium/README.md +++ b/selenium/README.md @@ -15,18 +15,15 @@ TCI for [Selenium](https://github.com/SeleniumHQ/selenium).
The configuration is dynamically loaded from (sorted by highest priority) * Environment variables - * prefixed with ``TCI_SELENIUM_`` + * prefixed with `TCI_SELENIUM_` * all properties are in UPPERCASE and use `_` instead of `.` or `-` * System properties - * prefixed with ``tci.selenium.`` + * prefixed with `tci.selenium.`
Full list of configuration options -_Please note that the preconfigured values usually work out of the box.
_ -_You should know exactly what you're doing when doing modifications._ - | Property | Type | Default | Notes | | --- | --- | --- | --- | | `record-mode` | `Enum` | `RECORD_FAILING` | Recording mode.
Available:
  • SKIP - Do not record any videos
  • RECORD_ALL - Record all tests
  • RECORD_FAILING - Record failing tests only
| @@ -35,3 +32,5 @@ _You should know exactly what you're doing when doing modifications._ | `bidi-enabled` | `bool` | `true` | Use [Selenium BiDirectional functionality](https://www.selenium.dev/documentation/webdriver/bidi/) instead of legacy Chrome DevTools Protocol (CDP).
Disabling this will make certain operations unavailable e.g. listening for browser logs. | | `deactivate-cdp-if-possible` | `bool` | `true` | Disable Chrome DevTools Protocol (CDP) if possible.
CDP requires additional maven dependencies (e.g. `selenium-devtools-v137)` that are not present by default and will result in a warning. | | `min-browser-console-log-level` | `Enum` | `ERROR` | Prints out browser console logs. Configures the MINIMUM log level.
Available options:
  • OFF
  • ERROR
  • WARN
  • INFO
  • DEBUG
  • ALL
+ +
diff --git a/selenium/src/main/java/software/xdev/tci/selenium/factory/config/DefaultBrowserTCIFactoryConfig.java b/selenium/src/main/java/software/xdev/tci/selenium/factory/config/DefaultBrowserTCIFactoryConfig.java index 22119602..316952ac 100644 --- a/selenium/src/main/java/software/xdev/tci/selenium/factory/config/DefaultBrowserTCIFactoryConfig.java +++ b/selenium/src/main/java/software/xdev/tci/selenium/factory/config/DefaultBrowserTCIFactoryConfig.java @@ -176,18 +176,4 @@ public BrowserTCIFactory.BrowserConsoleLogLevel minBrowserConsoleLogLevel() } return this.browserConsoleLogLevel; } - - protected T reportLegacyConfigOption(final String legacy, final String upToDate, final T value) - { - this.reportLegacyConfigOption(legacy, upToDate); - return value; - } - - protected void reportLegacyConfigOption(final String legacy, final String upToDate) - { - LOG.warn( - "Detected deprecated config option that will be removed: {} - use {} instead", - legacy, - upToDate); - } } From 63e25f75c05c7b855ef922205efcadb70c268a3d Mon Sep 17 00:00:00 2001 From: AB Date: Wed, 15 Jul 2026 09:37:18 +0200 Subject: [PATCH 127/141] Migrate existing properties --- .github/workflows/run-integration-tests.yml | 4 ++-- .../tci/factory/prestart/config/DefaultPreStartConfig.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/run-integration-tests.yml b/.github/workflows/run-integration-tests.yml index 228097ad..355161aa 100644 --- a/.github/workflows/run-integration-tests.yml +++ b/.github/workflows/run-integration-tests.yml @@ -80,7 +80,7 @@ jobs: ./mvnw -B test \ -pl "advanced-demo/integration-tests/${{ matrix.project }}" -am \ -P run-it \ - ${{ matrix.pre-start && '-Dinfra-pre-start.enabled=1 ' || '' }} \ + ${{ matrix.pre-start && '-Dtci.infra-pre-start.enabled=1 ' || '' }} \ ${{ matrix.parallel > 0 && format('-Djunit.jupiter.execution.parallel.enabled=true -Djunit.jupiter.execution.parallel.mode.default=concurrent -Djunit.jupiter.execution.parallel.mode.classes.default=concurrent -Djunit.jupiter.execution.parallel.config.strategy=fixed -Djunit.jupiter.execution.parallel.config.fixed.parallelism=2 -Djunit.jupiter.execution.parallel.config.fixed.max-pool-size={0} ', matrix.parallel) || '' }} env: ACTIONS_RUNTIME_TOKEN: ${{ steps.docker-image-cache-setup-buildx.outputs.ACTIONS_RUNTIME_TOKEN }} @@ -165,7 +165,7 @@ jobs: -Dtci.image-build.cache-to="type=gha,mode=max,scope=§image" \ -Dtci.image-build.tci-demo-webapp.cache-from="type=gha,scope=§image-j_avx${java_useavx}${{ matrix.jacoco && '-jacoco' || '' }}" \ -Dtci.image-build.tci-demo-webapp.cache-to="type=gha,mode=max,scope=§image-j_avx${java_useavx}${{ matrix.jacoco && '-jacoco' || '' }}" \ - ${{ matrix.pre-start && '-Dinfra-pre-start.enabled=1 ' || '' }} \ + ${{ matrix.pre-start && '-Dtci.infra-pre-start.enabled=1 ' || '' }} \ ${{ matrix.parallel > 0 && format('-Djunit.jupiter.execution.parallel.enabled=true -Djunit.jupiter.execution.parallel.mode.default=concurrent -Djunit.jupiter.execution.parallel.mode.classes.default=concurrent -Djunit.jupiter.execution.parallel.config.strategy=fixed -Djunit.jupiter.execution.parallel.config.fixed.parallelism=2 -Djunit.jupiter.execution.parallel.config.fixed.max-pool-size={0} ', matrix.parallel) || '' }} env: ACTIONS_RUNTIME_TOKEN: ${{ steps.docker-image-cache-setup-buildx.outputs.ACTIONS_RUNTIME_TOKEN }} diff --git a/base/src/main/java/software/xdev/tci/factory/prestart/config/DefaultPreStartConfig.java b/base/src/main/java/software/xdev/tci/factory/prestart/config/DefaultPreStartConfig.java index 7ffffbf6..ae19b745 100644 --- a/base/src/main/java/software/xdev/tci/factory/prestart/config/DefaultPreStartConfig.java +++ b/base/src/main/java/software/xdev/tci/factory/prestart/config/DefaultPreStartConfig.java @@ -25,8 +25,8 @@ *

* Properties can be defined in the following way: *

- * -Dinfra-pre-start.keep-ready=2
- * -Dinfra-pre-start.coordinator.idle-cpu-percent=50
+ * -Dtci.infra-pre-start.keep-ready=2
+ * -Dtci.infra-pre-start.coordinator.idle-cpu-percent=50
  * 
*

*/ From 92ef80d8d32954573347f954f6b5793dbb39382d Mon Sep 17 00:00:00 2001 From: AB Date: Wed, 15 Jul 2026 09:40:05 +0200 Subject: [PATCH 128/141] Improve table format --- base/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/base/README.md b/base/README.md index bc62c4c0..451318eb 100644 --- a/base/README.md +++ b/base/README.md @@ -44,8 +44,8 @@ _NOTE: `*` indicates that only some properties support this_ | Property | Type | Default | Notes | | --- | --- | --- | --- | | `enabled` | `bool` | `false` | Should PreStarting be enabled? | -| `keep-ready`* | `int` | [`junit.jupiter.execution.parallel.config.fixed.max-pool-size`](https://docs.junit.org/6.1.2/writing-tests/parallel-execution.html) or `1` | How many container should be kept ready for use in the background?
Setting this to a value `< 0` will effectively disable PreStarting | -| `max-start-simultan`* | `int` | [`junit.jupiter.execution.parallel.config.fixed.max-pool-size`](https://docs.junit.org/6.1.2/writing-tests/parallel-execution.html) or `1` | How many contains should be started simultaneously?
Setting a negative value will remove this limitation | +| `keep-ready`* | `int` | [`junit.jupiter.execution.parallel.`
`config.fixed.max-pool-size`](https://docs.junit.org/6.1.2/writing-tests/parallel-execution.html) or `1` | How many container should be kept ready for use in the background?
Setting this to a value `< 0` will effectively disable PreStarting | +| `max-start-simultan`* | `int` | [`junit.jupiter.execution.parallel.`
`config.fixed.max-pool-size`](https://docs.junit.org/6.1.2/writing-tests/parallel-execution.html) or `1` | How many contains should be started simultaneously?
Setting a negative value will remove this limitation | | `direct-network-attach-if-possible`* | `bool` | `true` |
  • true - Directly attaches the container to the network during startup if possible
  • false - Always performs a network-connect as if PreStarting is active. This is slower, however it emulates PreStarting better and may help with finding bugs.
| | `fixate-exposed-ports-if-required`* | `bool` | `true` | Fixates exposed ports when no direct network attach is possible. This is a workaround for moby/moby#44137. | | `coordinator.idle-cpu-percent` | `int` | `40`% | Amount of CPU that needs to be idle to allow PreStarting of containers | From 7f35e3ec2df4c27c7227229860f6e22a4164f34a Mon Sep 17 00:00:00 2001 From: AB Date: Wed, 15 Jul 2026 09:41:07 +0200 Subject: [PATCH 129/141] Update README.md --- base/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/README.md b/base/README.md index 451318eb..a8222fb2 100644 --- a/base/README.md +++ b/base/README.md @@ -45,7 +45,7 @@ _NOTE: `*` indicates that only some properties support this_ | --- | --- | --- | --- | | `enabled` | `bool` | `false` | Should PreStarting be enabled? | | `keep-ready`* | `int` | [`junit.jupiter.execution.parallel.`
`config.fixed.max-pool-size`](https://docs.junit.org/6.1.2/writing-tests/parallel-execution.html) or `1` | How many container should be kept ready for use in the background?
Setting this to a value `< 0` will effectively disable PreStarting | -| `max-start-simultan`* | `int` | [`junit.jupiter.execution.parallel.`
`config.fixed.max-pool-size`](https://docs.junit.org/6.1.2/writing-tests/parallel-execution.html) or `1` | How many contains should be started simultaneously?
Setting a negative value will remove this limitation | +| `max-start-simultan`* | `int` | [`junit.jupiter.execution.parallel.`
`config.fixed.max-pool-size`](https://docs.junit.org/6.1.2/writing-tests/parallel-execution.html) or `1` | Maximum amount of containers that should be started simultaneously
Setting a negative value will remove this limitation | | `direct-network-attach-if-possible`* | `bool` | `true` |
  • true - Directly attaches the container to the network during startup if possible
  • false - Always performs a network-connect as if PreStarting is active. This is slower, however it emulates PreStarting better and may help with finding bugs.
| | `fixate-exposed-ports-if-required`* | `bool` | `true` | Fixates exposed ports when no direct network attach is possible. This is a workaround for moby/moby#44137. | | `coordinator.idle-cpu-percent` | `int` | `40`% | Amount of CPU that needs to be idle to allow PreStarting of containers | From 4bced1a1bb71b9e0d4e03325813a744a46d0a4a6 Mon Sep 17 00:00:00 2001 From: AB Date: Wed, 15 Jul 2026 15:27:23 +0200 Subject: [PATCH 130/141] Added `mailpit` module --- .github/workflows/run-integration-tests.yml | 2 + .../run-mailpit-integration-tests.yml | 67 ++++ CHANGELOG.md | 1 + bom/pom.xml | 5 + mailpit/Dockerfile | 3 + mailpit/README.md | 3 + mailpit/pom.xml | 339 ++++++++++++++++++ .../software/xdev/tci/mailpit/MailpitTCI.java | 87 +++++ .../mailpit/containers/MailpitContainer.java | 60 ++++ .../mailpit/factory/MailpitTCIFactory.java | 56 +++ .../xdev/mailpit/SimpleMailpitTest.java | 130 +++++++ pom.xml | 1 + 12 files changed, 754 insertions(+) create mode 100644 .github/workflows/run-mailpit-integration-tests.yml create mode 100644 mailpit/Dockerfile create mode 100644 mailpit/README.md create mode 100644 mailpit/pom.xml create mode 100644 mailpit/src/main/java/software/xdev/tci/mailpit/MailpitTCI.java create mode 100644 mailpit/src/main/java/software/xdev/tci/mailpit/containers/MailpitContainer.java create mode 100644 mailpit/src/main/java/software/xdev/tci/mailpit/factory/MailpitTCIFactory.java create mode 100644 mailpit/src/test/java/software/xdev/mailpit/SimpleMailpitTest.java diff --git a/.github/workflows/run-integration-tests.yml b/.github/workflows/run-integration-tests.yml index 355161aa..6eb64a67 100644 --- a/.github/workflows/run-integration-tests.yml +++ b/.github/workflows/run-integration-tests.yml @@ -79,6 +79,7 @@ jobs: run: | ./mvnw -B test \ -pl "advanced-demo/integration-tests/${{ matrix.project }}" -am \ + -T2C \ -P run-it \ ${{ matrix.pre-start && '-Dtci.infra-pre-start.enabled=1 ' || '' }} \ ${{ matrix.parallel > 0 && format('-Djunit.jupiter.execution.parallel.enabled=true -Djunit.jupiter.execution.parallel.mode.default=concurrent -Djunit.jupiter.execution.parallel.mode.classes.default=concurrent -Djunit.jupiter.execution.parallel.config.strategy=fixed -Djunit.jupiter.execution.parallel.config.fixed.parallelism=2 -Djunit.jupiter.execution.parallel.config.fixed.max-pool-size={0} ', matrix.parallel) || '' }} @@ -159,6 +160,7 @@ jobs: ./mvnw -B test \ -pl "advanced-demo/integration-tests/${{ matrix.project }}" -am \ + -T2C \ -P run-it${{ matrix.jacoco && ',jacoco' || '' }} \ -Dtci.image-build.delete-on-exit=true \ -Dtci.image-build.cache-from="type=gha,scope=§image" \ diff --git a/.github/workflows/run-mailpit-integration-tests.yml b/.github/workflows/run-mailpit-integration-tests.yml new file mode 100644 index 00000000..97e0a4d2 --- /dev/null +++ b/.github/workflows/run-mailpit-integration-tests.yml @@ -0,0 +1,67 @@ +name: Run Mailpit integration tests + +on: + workflow_dispatch: + push: + branches: [ develop ] + paths: + - 'mailpit/**' + pull_request: + branches: [ develop ] + paths: + - 'mailpit/**' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + mailpit: + runs-on: ubuntu-latest + if: ${{ !(github.event_name == 'pull_request' && startsWith(github.head_ref, 'renovate/')) }} + timeout-minutes: 7 + permissions: + contents: read + steps: + - uses: actions/checkout@v7 + + - parallel: + - name: Set up JDK + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: ${{ matrix.java }} + + - name: Cache Maven + uses: actions/cache@v6 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-mvn-it-build-mailpit-${{ hashFiles('**/pom.xml') }} + + - name: Docker Image Cache Restore + id: docker-image-cache-restore + uses: ./.github/actions/docker-image-cache/restore + with: + # This cache could always change however because docker images can be changed remotely if the version is not fixated + # As a compromise it utilizes the hash of the pom files because when the dependencies stay the same + # the images should also be roughly the same + key: it-docker-images-mailpit-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-it-docker-images-mailpit-${{ hashFiles('**/pom.xml') }} + ${{ runner.os }}-it-docker-images-mailpit- + + - name: Test + run: | + ./mvnw -B test \ + -pl "mailpit" -am \ + -T2C \ + -P run-it + + - name: Show images + run: docker image ls + + - name: Docker Image Cache Save + uses: ./.github/actions/docker-image-cache/save + with: + key: ${{ steps.docker-image-cache-restore.outputs.key }} + cache-hit: ${{ steps.docker-image-cache-restore.outputs.cache-hit }} diff --git a/CHANGELOG.md b/CHANGELOG.md index dbe6104f..4bf5df13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ * This module utilizes v4 of [testcontainers-advanced-imagebuilder](https://github.com/xdev-software/testcontainers-advanced-imagebuilder) * It contains some shortcuts that help with common configuration e.g. related to caching (see below) * Can be configured using environment variables or system properties (see corresponding configuration section for details) +* Added `mailpit` module * Added dedicated composite actions for caching * These can be found in `.github/actions/docker-image-cache` * Currently 3 actions exist: diff --git a/bom/pom.xml b/bom/pom.xml index 7f6e1f83..e373dffe 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -93,6 +93,11 @@ junit-jupiter-api-support 4.0.0-SNAPSHOT + + software.xdev.tci + mailpit + 4.0.0-SNAPSHOT + software.xdev.tci mockserver diff --git a/mailpit/Dockerfile b/mailpit/Dockerfile new file mode 100644 index 00000000..3c9c49a3 --- /dev/null +++ b/mailpit/Dockerfile @@ -0,0 +1,3 @@ +# This file is just used as a reminder to update the latest minor version in +# MailpitContainer.java from time to time +FROM axllent/mailpit:v1.30 diff --git a/mailpit/README.md b/mailpit/README.md new file mode 100644 index 00000000..9d251cbb --- /dev/null +++ b/mailpit/README.md @@ -0,0 +1,3 @@ +# Mailpit + +TCI for [Mailpit](https://github.com/axllent/mailpit). diff --git a/mailpit/pom.xml b/mailpit/pom.xml new file mode 100644 index 00000000..8fd3b4cf --- /dev/null +++ b/mailpit/pom.xml @@ -0,0 +1,339 @@ + + + 4.0.0 + + software.xdev.tci + mailpit + 4.0.0-SNAPSHOT + jar + + mailpit + TCI - mailpit + https://github.com/xdev-software/tci + + + https://github.com/xdev-software/tci + scm:git:https://github.com/xdev-software/tci.git + + + 2025 + + + XDEV Software + https://xdev.software + + + + + XDEV Software + XDEV Software + https://xdev.software + + + + + + Apache-2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + 21 + ${javaVersion} + + UTF-8 + UTF-8 + + + true + + + + + software.xdev.tci + base + 4.0.0-SNAPSHOT + + + + software.xdev + mailpit-java-client + 1.0.0 + + + + + org.junit.jupiter + junit-jupiter + 6.1.2 + test + + + org.slf4j + slf4j-simple + 2.0.18 + test + + + + org.simplejavamail + simple-java-mail + 9.1.0 + test + + + + + + + + org.apache.maven.plugins + maven-site-plugin + 4.0.0-M16 + + + org.apache.maven.plugins + maven-project-info-reports-plugin + 3.9.0 + + + + + + com.mycila + license-maven-plugin + 5.0.0 + + + ${project.organization.url} + + + +
com/mycila/maven/plugin/license/templates/APACHE-2.txt
+ + src/main/java/** + src/test/java/** + +
+
+
+ + + first + + format + + process-sources + + +
+ + + org.apache.maven.plugins + maven-compiler-plugin + 3.15.0 + + ${maven.compiler.release} + + -proc:none + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.12.0 + + + attach-javadocs + package + + jar + + + + + true + none + + + + org.apache.maven.plugins + maven-source-plugin + 3.4.0 + + + attach-sources + package + + jar-no-fork + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.6 + + ${skipTests} + + +
+
+ + + ignore-service-loading + + + + src/main/resources + + META-INF/services/** + + + + + + + publish + + + + org.codehaus.mojo + flatten-maven-plugin + 1.7.3 + + ossrh + + + + flatten + process-resources + + flatten + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 3.2.8 + + + sign-artifacts + verify + + sign + + + + + + --pinentry-mode + loopback + + + + + + + + + + publish-sonatype-central-portal + + + + org.sonatype.central + central-publishing-maven-plugin + 0.11.0 + true + + sonatype-central-portal + true + + + + + + + checkstyle + + + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.6.0 + + + com.puppycrawl.tools + checkstyle + 13.8.0 + + + + ../.config/checkstyle/checkstyle.xml + true + + + + + check + + + + + + + + + pmd + + + + org.apache.maven.plugins + maven-pmd-plugin + 3.28.0 + + true + true + true + + ../.config/pmd/java/ruleset.xml + + + + + net.sourceforge.pmd + pmd-core + 7.26.0 + + + net.sourceforge.pmd + pmd-java + 7.26.0 + + + + + + + + + + org.apache.maven.plugins + maven-jxr-plugin + 3.6.0 + + + + + +
diff --git a/mailpit/src/main/java/software/xdev/tci/mailpit/MailpitTCI.java b/mailpit/src/main/java/software/xdev/tci/mailpit/MailpitTCI.java new file mode 100644 index 00000000..e63c7639 --- /dev/null +++ b/mailpit/src/main/java/software/xdev/tci/mailpit/MailpitTCI.java @@ -0,0 +1,87 @@ +/* + * Copyright © 2025 XDEV Software (https://xdev.software) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package software.xdev.tci.mailpit; + +import java.time.Duration; + +import org.apache.hc.client5.http.config.ConnectionConfig; +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; +import org.apache.hc.core5.util.Timeout; +import org.slf4j.LoggerFactory; + +import software.xdev.mailpit.client.ApiClient; +import software.xdev.tci.TCI; +import software.xdev.tci.mailpit.containers.MailpitContainer; + + +public class MailpitTCI extends TCI +{ + protected ApiClient apiClient; + + public MailpitTCI(final MailpitContainer container, final String networkAlias) + { + super(container, networkAlias); + } + + public String getExternalHTTPEndpoint() + { + return "http://" + + this.getContainer().getHost() + + ":" + + this.getContainer().getMappedPort(MailpitContainer.WEB_PORT); + } + + public ApiClient apiClient() + { + if(this.apiClient != null) + { + return this.apiClient; + } + + final Duration defaultTimeout = Duration.ofSeconds(30); + + this.apiClient = new ApiClient(); + this.apiClient.setHttpClient(HttpClientBuilder.create() + .setConnectionManager(PoolingHttpClientConnectionManagerBuilder.create() + .setDefaultConnectionConfig(ConnectionConfig.custom() + .setConnectTimeout(Timeout.of(defaultTimeout)) + .setSocketTimeout(Timeout.of(defaultTimeout)) + .build()) + .build()) + .build()); + this.apiClient.setBasePath(this.getExternalHTTPEndpoint()); + + return this.apiClient; + } + + @Override + public void stop() + { + if(this.apiClient != null) + { + try + { + this.apiClient.getHttpClient().close(); + } + catch(final Exception e) + { + LoggerFactory.getLogger(this.getClass()).warn("Failed to close API client", e); + } + } + super.stop(); + } +} diff --git a/mailpit/src/main/java/software/xdev/tci/mailpit/containers/MailpitContainer.java b/mailpit/src/main/java/software/xdev/tci/mailpit/containers/MailpitContainer.java new file mode 100644 index 00000000..785088f2 --- /dev/null +++ b/mailpit/src/main/java/software/xdev/tci/mailpit/containers/MailpitContainer.java @@ -0,0 +1,60 @@ +/* + * Copyright © 2025 XDEV Software (https://xdev.software) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package software.xdev.tci.mailpit.containers; + +import java.util.Map; +import java.util.stream.Collectors; + +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.utility.DockerImageName; + + +public class MailpitContainer extends GenericContainer +{ + public static final DockerImageName DEFAULT_IMAGE = DockerImageName.parse("axllent/mailpit:v1.30"); + + public static final int WEB_PORT = 8025; + public static final int SMTP_PORT = 1025; + + public MailpitContainer() + { + super(DEFAULT_IMAGE); + + // Version check is not needed in tests + this.addEnv("MP_DISABLE_VERSION_CHECK", "true"); + // Resolving client reverse dns is not needed + this.addEnv("MP_SMTP_DISABLE_RDNS", "true"); + + // Enforce CSP (only important when debugging) + this.addEnv("MP_BLOCK_REMOTE_CSS_AND_FONTS", "true"); + + // Use normal SMTP + this.addEnv("MP_SMTP_AUTH_ALLOW_INSECURE", "true"); + + this.addExposedPort(WEB_PORT); + } + + public MailpitContainer withSmtpAuth(final Map usernamePasswords) + { + this.addEnv( + "MP_SMTP_AUTH", + usernamePasswords.entrySet() + .stream() + .map(e -> e.getKey() + ":" + e.getValue()) + .collect(Collectors.joining(" "))); + return this; + } +} diff --git a/mailpit/src/main/java/software/xdev/tci/mailpit/factory/MailpitTCIFactory.java b/mailpit/src/main/java/software/xdev/tci/mailpit/factory/MailpitTCIFactory.java new file mode 100644 index 00000000..a5d172fc --- /dev/null +++ b/mailpit/src/main/java/software/xdev/tci/mailpit/factory/MailpitTCIFactory.java @@ -0,0 +1,56 @@ +/* + * Copyright © 2025 XDEV Software (https://xdev.software) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package software.xdev.tci.mailpit.factory; + +import java.util.Map; +import java.util.function.Supplier; + +import software.xdev.tci.factory.prestart.PreStartableTCIFactory; +import software.xdev.tci.mailpit.MailpitTCI; +import software.xdev.tci.mailpit.containers.MailpitContainer; +import software.xdev.tci.misc.ContainerMemory; + + +public class MailpitTCIFactory extends PreStartableTCIFactory +{ + public static final String DEFAULT_USER = "no-reply@test.localhost"; + public static final String DEFAULT_PW = "test"; + public static final int SMTP_PORT = MailpitContainer.SMTP_PORT; + + public MailpitTCIFactory() + { + this(MailpitTCIFactory::createDefaultContainer); + } + + public MailpitTCIFactory(final Supplier mailpitContainerSupplier) + { + super( + MailpitTCI::new, + mailpitContainerSupplier, + "mailpit", + "container.mailpit", + "Mailpit" + ); + } + + @SuppressWarnings("resource") + public static MailpitContainer createDefaultContainer() + { + return new MailpitContainer() + .withSmtpAuth(Map.of(DEFAULT_USER, DEFAULT_PW)) + .withCreateContainerCmdModifier(cmd -> cmd.getHostConfig().withMemory(ContainerMemory.M512M)); + } +} diff --git a/mailpit/src/test/java/software/xdev/mailpit/SimpleMailpitTest.java b/mailpit/src/test/java/software/xdev/mailpit/SimpleMailpitTest.java new file mode 100644 index 00000000..9752cb47 --- /dev/null +++ b/mailpit/src/test/java/software/xdev/mailpit/SimpleMailpitTest.java @@ -0,0 +1,130 @@ +package software.xdev.mailpit; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.Arrays; +import java.util.concurrent.CompletableFuture; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.simplejavamail.api.mailer.Mailer; +import org.simplejavamail.api.mailer.config.TransportStrategy; +import org.simplejavamail.email.EmailBuilder; +import org.simplejavamail.mailer.MailerBuilder; +import org.simplejavamail.recipient.RecipientBuilder; + +import software.xdev.mailpit.api.MessageApi; +import software.xdev.mailpit.client.ApiClient; +import software.xdev.mailpit.model.Message; +import software.xdev.tci.concurrent.TCIExecutorServiceHolder; +import software.xdev.tci.factory.registry.TCIFactoryRegistry; +import software.xdev.tci.mailpit.MailpitTCI; +import software.xdev.tci.mailpit.containers.MailpitContainer; +import software.xdev.tci.mailpit.factory.MailpitTCIFactory; +import software.xdev.tci.network.LazyNetwork; +import software.xdev.tci.network.LazyNetworkPool; + + +class SimpleMailpitTest +{ + protected static final MailpitTCIFactory FACTORY = new MailpitTCIFactory(() -> { + final MailpitContainer container = MailpitTCIFactory.createDefaultContainer(); + // For testing we send mails form the host so SMTP must be exposed + container.addExposedPort(MailpitTCIFactory.SMTP_PORT); + return container; + }); + protected static final LazyNetworkPool LAZY_NETWORK_POOL = new LazyNetworkPool(); + + protected LazyNetwork network; + protected MailpitTCI mailpitInfra; + + @BeforeAll + static void beforeAll() + { + LAZY_NETWORK_POOL.managePoolAsync(); + + TCIFactoryRegistry.instance().warmUp(); + + // Preload classes + CompletableFuture.runAsync(ApiClient::new, TCIExecutorServiceHolder.instance()); + } + + @BeforeEach + void beforeEach() + { + this.network = LAZY_NETWORK_POOL.getNew(); + this.mailpitInfra = FACTORY.getNew(this.network); + } + + @Test + void check() + { + final String toAddress = "m.mustermann@test.localhost"; + final String subject = "Test"; + final String plainText = "This is a test"; + + try(final Mailer mailer = MailerBuilder.withSMTPServer( + this.mailpitInfra.getContainer().getHost(), + this.mailpitInfra.getContainer().getMappedPort(MailpitTCIFactory.SMTP_PORT), + MailpitTCIFactory.DEFAULT_USER, + MailpitTCIFactory.DEFAULT_PW) + .withTransportStrategy(TransportStrategy.SMTP) + .withDebugLogging(true) + .buildMailer()) + { + mailer.sendMail(EmailBuilder.startingBlank() + .from(MailpitTCIFactory.DEFAULT_USER) + .withRecipients(new RecipientBuilder() + .withType(jakarta.mail.Message.RecipientType.TO) + .withAddress(toAddress) + .withName("Max Mustermann") + .build()) + .withSubject(subject) + .withPlainText(plainText) + .buildEmail()); + } + catch(final Exception ex) + { + throw new IllegalStateException("Mailer failed", ex); + } + + final Message message = new MessageApi(this.mailpitInfra.apiClient()).getMessageParams("latest"); + assertNotNull(message); + assertAll( + () -> assertEquals(toAddress, message.getTo().getFirst().getAddress()), + () -> assertEquals(subject, message.getSubject()), + () -> assertEquals( + plainText, + Arrays.stream(message.getText().split("\n")) + .map(String::trim) + .findFirst() + .orElse(null)) + ); + } + + @AfterEach + void afterEach() + { + if(this.mailpitInfra != null) + { + this.mailpitInfra.stop(); + this.mailpitInfra = null; + } + if(this.network != null) + { + this.network.close(); + this.network = null; + } + } + + @AfterAll + static void afterAll() + { + FACTORY.close(); + } +} diff --git a/pom.xml b/pom.xml index fdc2b45a..d4bf205d 100644 --- a/pom.xml +++ b/pom.xml @@ -25,6 +25,7 @@ jacoco jul-to-slf4j junit-jupiter-api-support + mailpit mockserver oidc-server-mock selenium From 490b2a74a73256bea7d8afe4ec1dd0a1d5294daa Mon Sep 17 00:00:00 2001 From: AB Date: Wed, 15 Jul 2026 15:28:55 +0200 Subject: [PATCH 131/141] Add missing license header --- .../software/xdev/mailpit/SimpleMailpitTest.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/mailpit/src/test/java/software/xdev/mailpit/SimpleMailpitTest.java b/mailpit/src/test/java/software/xdev/mailpit/SimpleMailpitTest.java index 9752cb47..ff666022 100644 --- a/mailpit/src/test/java/software/xdev/mailpit/SimpleMailpitTest.java +++ b/mailpit/src/test/java/software/xdev/mailpit/SimpleMailpitTest.java @@ -1,3 +1,18 @@ +/* + * Copyright © 2025 XDEV Software (https://xdev.software) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package software.xdev.mailpit; import static org.junit.jupiter.api.Assertions.assertAll; From c39a389f16bee755c3af078ddb912ea1d11084ed Mon Sep 17 00:00:00 2001 From: AB Date: Wed, 15 Jul 2026 15:29:51 +0200 Subject: [PATCH 132/141] Fix syntax --- .github/workflows/run-mailpit-integration-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-mailpit-integration-tests.yml b/.github/workflows/run-mailpit-integration-tests.yml index 97e0a4d2..dbb22111 100644 --- a/.github/workflows/run-mailpit-integration-tests.yml +++ b/.github/workflows/run-mailpit-integration-tests.yml @@ -30,7 +30,7 @@ jobs: uses: actions/setup-java@v5 with: distribution: temurin - java-version: ${{ matrix.java }} + java-version: 25 - name: Cache Maven uses: actions/cache@v6 From 67cba02c46cbba70f9031a0ec5b04bf24abe5447 Mon Sep 17 00:00:00 2001 From: AB Date: Wed, 15 Jul 2026 15:32:17 +0200 Subject: [PATCH 133/141] Fix workflow exec --- .github/actions/docker-image-cache/save/action.yml | 2 +- mailpit/pom.xml | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/actions/docker-image-cache/save/action.yml b/.github/actions/docker-image-cache/save/action.yml index 4d21716d..de54456d 100644 --- a/.github/actions/docker-image-cache/save/action.yml +++ b/.github/actions/docker-image-cache/save/action.yml @@ -34,7 +34,7 @@ runs: image_ids_to_save=$(comm -23 <(echo "${image_ids_to_save[@]}") <(cat ~/.cache/docker-preinstalled/ids-${{ github.run_id }}.txt || echo "")) fi echo && echo "Saving image ids" && echo $image_ids_to_save - docker save $image_ids_to_save -o ~/.cache/docker/images.tar + docker save $image_ids_to_save -o ~/.cache/docker/images.tar || true ls -lha ~/.cache/docker if [ "$has_preinstalled_ids" = true ]; then diff --git a/mailpit/pom.xml b/mailpit/pom.xml index 8fd3b4cf..180b2c1b 100644 --- a/mailpit/pom.xml +++ b/mailpit/pom.xml @@ -187,6 +187,12 @@ + + run-it + + false + + ignore-service-loading From 1b945abbf90dcd82ef7653f218ea88522fb53570 Mon Sep 17 00:00:00 2001 From: AB Date: Wed, 15 Jul 2026 15:37:04 +0200 Subject: [PATCH 134/141] Add proper logging for tests --- mailpit/pom.xml | 15 +++++++++--- mailpit/src/test/resources/log4j2-test.xml | 28 ++++++++++++++++++++++ 2 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 mailpit/src/test/resources/log4j2-test.xml diff --git a/mailpit/pom.xml b/mailpit/pom.xml index 180b2c1b..1edbaf10 100644 --- a/mailpit/pom.xml +++ b/mailpit/pom.xml @@ -50,6 +50,8 @@ true + + 2.26.1 @@ -72,10 +74,17 @@ 6.1.2 test
+ + + org.apache.logging.log4j + log4j-core + ${log4j.version} + test + - org.slf4j - slf4j-simple - 2.0.18 + org.apache.logging.log4j + log4j-slf4j2-impl + ${log4j.version} test diff --git a/mailpit/src/test/resources/log4j2-test.xml b/mailpit/src/test/resources/log4j2-test.xml new file mode 100644 index 00000000..3b76bc01 --- /dev/null +++ b/mailpit/src/test/resources/log4j2-test.xml @@ -0,0 +1,28 @@ + + + + + + %d{HH:mm:ss} %-5p [%t] [%-25.25c] %m %n + + + + + + + + + + + + + + + + + + + + + + From e239d144ff1838b8eb390ecb3755eea848b9d9c5 Mon Sep 17 00:00:00 2001 From: AB Date: Wed, 15 Jul 2026 15:39:31 +0200 Subject: [PATCH 135/141] Skip license update in tests --- .github/workflows/run-integration-tests.yml | 2 ++ .github/workflows/run-mailpit-integration-tests.yml | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/run-integration-tests.yml b/.github/workflows/run-integration-tests.yml index 6eb64a67..a458c8e3 100644 --- a/.github/workflows/run-integration-tests.yml +++ b/.github/workflows/run-integration-tests.yml @@ -81,6 +81,7 @@ jobs: -pl "advanced-demo/integration-tests/${{ matrix.project }}" -am \ -T2C \ -P run-it \ + -Dlicense.skip \ ${{ matrix.pre-start && '-Dtci.infra-pre-start.enabled=1 ' || '' }} \ ${{ matrix.parallel > 0 && format('-Djunit.jupiter.execution.parallel.enabled=true -Djunit.jupiter.execution.parallel.mode.default=concurrent -Djunit.jupiter.execution.parallel.mode.classes.default=concurrent -Djunit.jupiter.execution.parallel.config.strategy=fixed -Djunit.jupiter.execution.parallel.config.fixed.parallelism=2 -Djunit.jupiter.execution.parallel.config.fixed.max-pool-size={0} ', matrix.parallel) || '' }} env: @@ -162,6 +163,7 @@ jobs: -pl "advanced-demo/integration-tests/${{ matrix.project }}" -am \ -T2C \ -P run-it${{ matrix.jacoco && ',jacoco' || '' }} \ + -Dlicense.skip \ -Dtci.image-build.delete-on-exit=true \ -Dtci.image-build.cache-from="type=gha,scope=§image" \ -Dtci.image-build.cache-to="type=gha,mode=max,scope=§image" \ diff --git a/.github/workflows/run-mailpit-integration-tests.yml b/.github/workflows/run-mailpit-integration-tests.yml index dbb22111..7278bcc1 100644 --- a/.github/workflows/run-mailpit-integration-tests.yml +++ b/.github/workflows/run-mailpit-integration-tests.yml @@ -55,7 +55,8 @@ jobs: ./mvnw -B test \ -pl "mailpit" -am \ -T2C \ - -P run-it + -P run-it \ + -Dlicense.skip - name: Show images run: docker image ls From 85bc970f38ce6c688d26c9fc899b679d7586700e Mon Sep 17 00:00:00 2001 From: AB Date: Wed, 15 Jul 2026 15:45:58 +0200 Subject: [PATCH 136/141] Do not log shutdown on warn --- .../xdev/tci/factory/prestart/PreStartableTCIFactory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/src/main/java/software/xdev/tci/factory/prestart/PreStartableTCIFactory.java b/base/src/main/java/software/xdev/tci/factory/prestart/PreStartableTCIFactory.java index 0b1ed959..b9329f28 100644 --- a/base/src/main/java/software/xdev/tci/factory/prestart/PreStartableTCIFactory.java +++ b/base/src/main/java/software/xdev/tci/factory/prestart/PreStartableTCIFactory.java @@ -484,7 +484,7 @@ protected boolean isPreStartingDisabled() @Override public void close() { - this.log().warn("[{}] Shutting down", this.name); + this.log().info("[{}] Shutting down", this.name); if(!this.isPreStartingDisabled()) { GlobalPreStartCoordinator.instance().unregister(this); From 729806c3601ea56f3d770bbe411e1cab62f1d714 Mon Sep 17 00:00:00 2001 From: AB Date: Wed, 15 Jul 2026 15:53:04 +0200 Subject: [PATCH 137/141] Reference issue --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bf5df13..a84d54bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ * This module utilizes v4 of [testcontainers-advanced-imagebuilder](https://github.com/xdev-software/testcontainers-advanced-imagebuilder) * It contains some shortcuts that help with common configuration e.g. related to caching (see below) * Can be configured using environment variables or system properties (see corresponding configuration section for details) -* Added `mailpit` module +* Added `mailpit` module #455 * Added dedicated composite actions for caching * These can be found in `.github/actions/docker-image-cache` * Currently 3 actions exist: From f340646eafe6809a5c801f63b821e4d0e3a69a94 Mon Sep 17 00:00:00 2001 From: AB Date: Wed, 15 Jul 2026 15:56:33 +0200 Subject: [PATCH 138/141] Log reported java AVX level --- .github/workflows/run-integration-tests.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/run-integration-tests.yml b/.github/workflows/run-integration-tests.yml index a458c8e3..2bd32b26 100644 --- a/.github/workflows/run-integration-tests.yml +++ b/.github/workflows/run-integration-tests.yml @@ -153,11 +153,13 @@ jobs: - name: Test run: | - # Report CPU + # Report CPU details cat /proc/cpuinfo | grep 'name' | uniq + cat /proc/cpuinfo | grep 'flags' | uniq # Differentiate cache between AVX microcode used by Java (until https://github.com/xdev-software/tci/issues/688 is fixed) java_useavx=$(java -XX:+PrintFlagsFinal -version 2> /dev/null | awk '/UseAVX/ {print $4}') + echo "Java UseAVX: ${java_useavx}" ./mvnw -B test \ -pl "advanced-demo/integration-tests/${{ matrix.project }}" -am \ From 676de0c3e8ac3d364f084fc3be994ace6f64dda3 Mon Sep 17 00:00:00 2001 From: AB Date: Wed, 15 Jul 2026 16:02:18 +0200 Subject: [PATCH 139/141] Add dedicated job for CPU details Because when the job is running the "Test" step will get cut off --- .github/workflows/run-integration-tests.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run-integration-tests.yml b/.github/workflows/run-integration-tests.yml index 2bd32b26..e37d1e97 100644 --- a/.github/workflows/run-integration-tests.yml +++ b/.github/workflows/run-integration-tests.yml @@ -151,12 +151,13 @@ jobs: id: docker-image-cache-setup-buildx uses: ./.github/actions/docker-image-cache/setup-buildx - - name: Test + - name: Report CPU details run: | - # Report CPU details cat /proc/cpuinfo | grep 'name' | uniq cat /proc/cpuinfo | grep 'flags' | uniq + - name: Test + run: | # Differentiate cache between AVX microcode used by Java (until https://github.com/xdev-software/tci/issues/688 is fixed) java_useavx=$(java -XX:+PrintFlagsFinal -version 2> /dev/null | awk '/UseAVX/ {print $4}') echo "Java UseAVX: ${java_useavx}" From 14febbd50a80060abe316f27a384289dd71f2276 Mon Sep 17 00:00:00 2001 From: AB Date: Wed, 15 Jul 2026 16:09:45 +0200 Subject: [PATCH 140/141] Correctly grep when Intel CPU is present --- .github/workflows/run-integration-tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run-integration-tests.yml b/.github/workflows/run-integration-tests.yml index e37d1e97..2f5724a5 100644 --- a/.github/workflows/run-integration-tests.yml +++ b/.github/workflows/run-integration-tests.yml @@ -153,8 +153,8 @@ jobs: - name: Report CPU details run: | - cat /proc/cpuinfo | grep 'name' | uniq - cat /proc/cpuinfo | grep 'flags' | uniq + cat /proc/cpuinfo | grep -P '^name' | uniq + cat /proc/cpuinfo | grep -P '^flags' | uniq - name: Test run: | From cb0138d10397e36ab875ef8ec0668c5f660582e6 Mon Sep 17 00:00:00 2001 From: AB Date: Wed, 15 Jul 2026 16:18:49 +0200 Subject: [PATCH 141/141] Update run-integration-tests.yml --- .github/workflows/run-integration-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-integration-tests.yml b/.github/workflows/run-integration-tests.yml index 2f5724a5..4d8deaa7 100644 --- a/.github/workflows/run-integration-tests.yml +++ b/.github/workflows/run-integration-tests.yml @@ -153,7 +153,7 @@ jobs: - name: Report CPU details run: | - cat /proc/cpuinfo | grep -P '^name' | uniq + cat /proc/cpuinfo | grep -P '^model name' | uniq cat /proc/cpuinfo | grep -P '^flags' | uniq - name: Test