This directory contains integration tests for the Java buildpack using the Switchblade framework.
Switchblade is a Go-based integration testing framework that supports both Cloud Foundry and Docker platforms. This allows us to write tests once and run them on either platform.
- Go 1.25 or later
- Cloud Foundry CLI (if testing on CF)
- Docker (if testing on Docker)
- A packaged buildpack zip file
- GitHub Personal Access Token (required for Docker platform tests)
- Create token at: https://github.com/settings/tokens
- Requires
public_repoorreposcope - Used to query buildpack metadata from GitHub API
First, create a buildpack zip file:
bundle exec rake packageThis will create a file like java-buildpack-v4.x.x.zip in the project root.
Use the provided script to run the tests:
# Test on Cloud Foundry (default)
BUILDPACK_FILE=/path/to/java-buildpack-v4.x.x.zip ./scripts/integration.sh
# Test on Docker (requires GitHub token)
BUILDPACK_FILE=/path/to/java-buildpack-v4.x.x.zip \
GITHUB_TOKEN=your_github_token_here \
./scripts/integration.sh --platform docker
# Run cached/offline tests
BUILDPACK_FILE=/path/to/java-buildpack-v4.x.x.zip ./scripts/integration.sh --cached
# Specify a different stack
BUILDPACK_FILE=/path/to/java-buildpack-v4.x.x.zip ./scripts/integration.sh --stack cflinuxfs4You can also run the tests directly using Go:
cd src/integration
# Run all tests
BUILDPACK_FILE=/path/to/buildpack.zip go test -mod vendor -v -timeout 30m ./
# Run on Docker (requires GitHub token)
BUILDPACK_FILE=/path/to/buildpack.zip go test -mod vendor -v -timeout 30m ./ \
-platform=docker -github-token=<token>
# Run offline tests
BUILDPACK_FILE=/path/to/buildpack.zip go test -mod vendor -v -timeout 30m ./ -cachedThe spec framework creates subtests 5 levels deep:
TestIntegration/integration/<Suite>/<context>/<it>
Use -run with a regex; Go does substring matching at each /-separated level:
# All SpringBoot real-jar tests
-run "TestIntegration/integration/SpringBoot/with_real_Spring_Boot_fat_jars"
# Single test — SB3 only
-run "TestIntegration/integration/SpringBoot/with_real_Spring_Boot_fat_jars/SB3"
# Single test — SB4 cfenv + loaded-jars
-run "TestIntegration/integration/SpringBoot/with_real_Spring_Boot_fat_jars/SB4.*cloud_profile"
# Single test — SB4 combined (cfenv + csp + metrics)
-run "TestIntegration/integration/SpringBoot/with_real_Spring_Boot_fat_jars/SB4.*container-security"
# All Tomcat tests
-run "TestIntegration/integration/Tomcat"
# All Frameworks tests
-run "TestIntegration/integration/Frameworks"init_test.go- Test suite initialization and configurationtomcat_test.go- Tomcat container testsspring_boot_test.go- Spring Boot application testsjava_main_test.go- Java Main class application testsoffline_test.go- Offline/cached buildpack tests
Tests use fixtures from the spec/fixtures directory. The main fixture for integration tests is:
integration_valid- A simple Java application with a Main-Class
BUILDPACK_FILE(required) - Path to the packaged buildpack zip filePLATFORM- Platform to test against:cf(default) ordockerSTACK- Stack to use for tests (default:cflinuxfs4)CACHED- Run offline/cached tests (default:false)GITHUB_TOKEN- GitHub API token to avoid rate limiting
-platform- Platform type (cfordocker)-stack- Stack name (e.g.,cflinuxfs4)-cached- Enable offline tests-github-token- GitHub API token-serial- Run tests serially instead of in parallel
The integration tests cover:
-
Container Types
- Tomcat container with WAR files
- Spring Boot executable JARs
- Java Main applications
-
JRE Selection
- Java 8, 11, 17 runtime selection
- Multiple JRE vendors (OpenJDK, Zulu, etc.)
-
Configuration
- Memory calculator settings
- Custom JAVA_OPTS
- Framework-specific configuration
-
Offline Mode
- Cached buildpack deployment
- No internet access scenarios
The #1301 work removed eval from the start command: JAVA_OPTS is now assembled
by a pure-bash expander and tokenized at launch by the shell-free javaexec launcher.
The exact-character behaviour this guarantees — shell metacharacters (; & | > ),
glob/cron stars, quotes-with-spaces, and backslashes all reaching the JVM as literal
text — is verified deterministically at the unit level in
../java/frameworks/java_opts_writer_test.go,
which runs the real generated assembly script and the real javaexec
tokenizer end-to-end.
Do not re-assert those exact characters through a docker deployment here. The
switchblade docker harness passes env vars into the container in a way that mangles
some metacharacters (e.g. a user JAVA_OPTS & was observed arriving as \& inside
the container) — that is a harness artifact, not buildpack behaviour (the unit
test above confirms the buildpack delivers a clean &). A docker integration test
asserting the literal received value would fail for reasons unrelated to the product.
What this directory does cover for #1301, because it is a genuine end-to-end
property that must hold in a real container launch:
- Command substitution is never executed — a
$(...)in userJAVA_OPTSreaches the JVM as literal text (SpringBootsuite, asserted via the fixture's/jvm-argsendpoint). - The
javaexeclauncher actually starts the JVM for the affected containers, includingPlay(previouslyeval exec java $JAVA_OPTS ...).
Source/git buildpack path (bin/finalize building javaexec on the fly) is
not covered by integration tests here: they use a packaged zip where bin/javaexec
is pre-built by scripts/build.sh. The source/git path is covered by unit tests in
../java/finalize/finalize_test.go and can be
verified locally with bash scripts/test-javaexec-source-path.sh.
To add a new test:
- Create a new test file in
src/integration/(e.g.,myfeature_test.go) - Define a test function that returns
func(*testing.T, spec.G, spec.S):func testMyFeature(platform switchblade.Platform, fixtures string) func(*testing.T, spec.G, spec.S) { return func(t *testing.T, context spec.G, it spec.S) { // Your tests here } }
- Register the test in
init_test.go:suite("MyFeature", testMyFeature(platform, fixtures))
To integrate with CI/CD pipelines:
# Example GitHub Actions
- name: Run Integration Tests
env:
BUILDPACK_FILE: ${{ github.workspace }}/java-buildpack.zip
CF_API: ${{ secrets.CF_API }}
CF_USERNAME: ${{ secrets.CF_USERNAME }}
CF_PASSWORD: ${{ secrets.CF_PASSWORD }}
run: |
./scripts/integration.sh --platform cfThe previous integration tests were:
- Located in a separate repository (
java-buildpack-system-test) - Written in Java with JUnit
- Only supported Cloud Foundry
- Required extensive configuration
The new Switchblade-based tests:
- Are co-located with the buildpack code
- Written in Go with Gomega matchers
- Support both Cloud Foundry and Docker
- Have simpler configuration and setup
go mod tidy
go mod downloadEnsure the BUILDPACK_FILE environment variable points to a valid zip file:
ls -lh $BUILDPACK_FILEEnsure you're logged into Cloud Foundry:
cf login -a <api-endpoint>Ensure Docker is running and you have permission to use it:
docker ps- The following error appears during staging while running an integration test:
Output:
{"status":"Pulling from cloudfoundry/cflinuxfs4","id":"latest"}
{"status":"Digest: sha256:77bf7297d2fbb4b787b73df2a4d0a911e5f0695321a6f0219a44c19be5d6bebe"}
{"status":"Status: Image is up to date for cloudfoundry/cflinuxfs4:latest"}
-----> Java Buildpack version dev
-----> Supplying Java
Detected container: Tomcat
No JRE explicitly configured, using default: OpenJDK
Selected JRE: OpenJDK
-----> Installing OpenJDK JRE
Installing OpenJDK 8.0.452
-----> Installing openjdk 8.0.452
Download [https://java-buildpack.cloudfoundry.org/openjdk/jammy/x86_64/bellsoft-jre8u452%2B11-linux-amd64.tar.gz]
error: Get "https://java-buildpack.cloudfoundry.org/openjdk/jammy/x86_64/bellsoft-jre8u452%2B11-linux-amd64.tar.gz": dial tcp 104.18.17.211:443: connect: no route to host, retrying in 712.622241ms...
Check whether the URL, for which the issue appears, is accessible from the host machine. If the URL appears reachable and can be accessed successfully, check again from within the corresponding Switchblade container where the test is run. This can be achieved with docker exec -it <container_id> /bin/bash while the container is still up in order to access the container interactively and, for example, issue a curl to the URL from within it. If the connect: no route to host can be reproduced from within the corresponding Switchblade container, you can try the following in order to mitigate it:
- Execute
docker network prune- this will remove any unused networks includingswitchblade-internalbridge networks set while running the integration tests. - Execute
sudo systemctl restart docker- this restarts Docker and resets its networking stack, which can resolve stale or broken network routes in the Docker daemon.
- Integration test is executed successfully but the following issue appears on test container removal:
tomcat_test.go:34:
Expected success, but got an error:
<*fmt.wrapError | 0xc00034f140>:
failed to run teardown phase: failed to remove container: Error response from daemon: cannot remove container "switchblade-pqsal7svg": could not kill container: permission denied
{
msg: "failed to run teardown phase: failed to remove container: Error response from daemon: cannot remove container \"switchblade-pqsal7svg\": could not kill container: permission denied",
err: <*fmt.wrapError | 0xc00034f0c0>{
msg: "failed to remove container: Error response from daemon: cannot remove container \"switchblade-pqsal7svg\": could not kill container: permission denied",
err: <errdefs.errSystem>{
error: <*errors.withStack | 0xc000405b60>{
error: <*errors.withMessage | 0xc00034f000>{
cause: <*errors.fundamental | 0xc000405b30>{
msg: "cannot remove container \"switchblade-pqsal7svg\": could not kill container: permission denied",
stack: [0x795cc0, 0x79517c, 0x790307, 0x7902a3, 0x7a2263, 0x7a807a, 0x7f6c2b, 0x7ad476, 0x7ad39f, 0x7acb35, 0x7abdb5, 0x52e46a, 0x485c21],
},
msg: "Error response from daemon",
},
stack: [0x795de5, 0x79517c, 0x790307, 0x7902a3, 0x7a2263, 0x7a807a, 0x7f6c2b, 0x7ad476, 0x7ad39f, 0x7acb35, 0x7abdb5, 0x52e46a, 0x485c21],
},
},
},
}
To mitigate the above issue try executing sudo systemctl restart docker.socket docker.service. This command restarts both the Docker systemd socket unit (which accepts client connections) and the main Docker daemon service. Doing so refreshes the daemon’s runtime state and can clear stale processes, file handles, or permission-related inconsistencies that prevent containers from being stopped or removed, resolving the permission denied error seen during teardown.
If you see errors like "Bad credentials" or "401 Unauthorized" when running Docker platform tests:
failed to build buildpacks: failed to list buildpacks: received unexpected response status: HTTP/2.0 401 Unauthorized
This means you need to provide a GitHub Personal Access Token:
- Create a token at https://github.com/settings/tokens
- Grant it
public_repoorreposcope - Export it as an environment variable:
export GITHUB_TOKEN=your_token_here BUILDPACK_FILE=/path/to/buildpack.zip ./scripts/integration.sh --platform docker
Alternatively, pass it via the command line:
BUILDPACK_FILE=/path/to/buildpack.zip ./scripts/integration.sh --platform docker --github-token your_token_here