Add Paketo image building system tests

A new system test plugin is being made available for running test
suites that should be run less frequently than with every commit, such
as tests that verify Spring Boot compatibility with external
projects. CI pipeline configuration for running system tests is also
provided.

The first system tests verify the behavior of the Spring Boot image
building plugins when building images using Paketo buildpacks.

Closes gh-25824
This commit is contained in:
Scott Frederick 2021-07-08 16:23:43 -05:00
parent d82b46b718
commit 18c7c0af22
51 changed files with 1326 additions and 73 deletions

View File

@ -67,6 +67,10 @@ gradlePlugin {
id = "org.springframework.boot.integration-test"
implementationClass = "org.springframework.boot.build.test.IntegrationTestPlugin"
}
systemTestPlugin {
id = "org.springframework.boot.system-test"
implementationClass = "org.springframework.boot.build.test.SystemTestPlugin"
}
mavenPluginPlugin {
id = "org.springframework.boot.maven-plugin"
implementationClass = "org.springframework.boot.build.mavenplugin.MavenPluginPlugin"

View File

@ -0,0 +1,83 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* 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
*
* https://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 org.springframework.boot.build.test;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.plugins.JavaPlugin;
import org.gradle.api.plugins.JavaPluginConvention;
import org.gradle.api.tasks.SourceSet;
import org.gradle.api.tasks.SourceSetContainer;
import org.gradle.api.tasks.testing.Test;
import org.gradle.language.base.plugins.LifecycleBasePlugin;
import org.gradle.plugins.ide.eclipse.EclipsePlugin;
import org.gradle.plugins.ide.eclipse.model.EclipseModel;
/**
* A {@link Plugin} to configure system testing support in a {@link Project}.
*
* @author Andy Wilkinson
* @author Scott Frederick
*/
public class SystemTestPlugin implements Plugin<Project> {
/**
* Name of the {@code systemTest} task.
*/
public static String SYSTEM_TEST_TASK_NAME = "systemTest";
/**
* Name of the {@code systemTest} source set.
*/
public static String SYSTEM_TEST_SOURCE_SET_NAME = "systemTest";
@Override
public void apply(Project project) {
project.getPlugins().withType(JavaPlugin.class, (javaPlugin) -> configureSystemTesting(project));
}
private void configureSystemTesting(Project project) {
SourceSet systemTestSourceSet = createSourceSet(project);
createTestTask(project, systemTestSourceSet);
project.getPlugins().withType(EclipsePlugin.class, (eclipsePlugin) -> {
EclipseModel eclipse = project.getExtensions().getByType(EclipseModel.class);
eclipse.classpath((classpath) -> classpath.getPlusConfigurations().add(
project.getConfigurations().getByName(systemTestSourceSet.getRuntimeClasspathConfigurationName())));
});
}
private SourceSet createSourceSet(Project project) {
SourceSetContainer sourceSets = project.getConvention().getPlugin(JavaPluginConvention.class).getSourceSets();
SourceSet systemTestSourceSet = sourceSets.create(SYSTEM_TEST_SOURCE_SET_NAME);
SourceSet mainSourceSet = sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME);
systemTestSourceSet
.setCompileClasspath(systemTestSourceSet.getCompileClasspath().plus(mainSourceSet.getOutput()));
systemTestSourceSet
.setRuntimeClasspath(systemTestSourceSet.getRuntimeClasspath().plus(mainSourceSet.getOutput()));
return systemTestSourceSet;
}
private void createTestTask(Project project, SourceSet systemTestSourceSet) {
Test systemTest = project.getTasks().create(SYSTEM_TEST_TASK_NAME, Test.class);
systemTest.setGroup(LifecycleBasePlugin.VERIFICATION_GROUP);
systemTest.setDescription("Runs system tests.");
systemTest.setTestClassesDirs(systemTestSourceSet.getOutput().getClassesDirs());
systemTest.setClasspath(systemTestSourceSet.getRuntimeClasspath());
systemTest.shouldRunAfter(JavaPlugin.TEST_TASK_NAME);
}
}

View File

@ -41,6 +41,14 @@ anchors:
BRANCH: ((branch))
<<: *gradle-enterprise-task-params
<<: *docker-hub-task-params
run-system-tests-task-params: &run-system-tests-task-params
privileged: true
timeout: ((task-timeout))
file: git-repo/ci/tasks/run-system-tests.yml
params:
BRANCH: ((branch))
<<: *gradle-enterprise-task-params
<<: *docker-hub-task-params
artifactory-repo-put-params: &artifactory-repo-put-params
signing_key: ((signing-key))
signing_passphrase: ((signing-passphrase))
@ -165,6 +173,12 @@ resources:
source:
<<: *registry-image-resource-source
repository: ((docker-hub-organization))/spring-boot-ci-jdk16
- name: paketo-builder-base-image
type: registry-image
icon: docker
source:
repository: paketobuildpacks/builder
tag: base
- name: artifactory-repo
type: artifactory-resource
icon: package-variant
@ -668,11 +682,86 @@ jobs:
- put: homebrew-tap-repo
params:
repository: updated-homebrew-tap-repo
- name: run-system-tests
serial: true
public: true
plan:
- get: ci-image
- get: git-repo
- get: paketo-builder-base-image
trigger: true
- get: daily
trigger: true
- do:
- task: run-system-tests
image: ci-image
<<: *run-system-tests-task-params
on_failure:
do:
- put: slack-alert
params:
<<: *slack-fail-params
- put: slack-alert
params:
<<: *slack-success-params
- name: jdk11-run-system-tests
serial: true
public: true
plan:
- get: ci-image-jdk11
- get: git-repo
- get: paketo-builder-base-image
trigger: true
- get: daily
trigger: true
- do:
- task: run-system-tests
image: ci-image-jdk11
<<: *run-system-tests-task-params
on_failure:
do:
- put: slack-alert
params:
<<: *slack-fail-params
- put: slack-alert
params:
<<: *slack-success-params
- name: jdk16-run-system-tests
serial: true
public: true
plan:
- get: ci-image-jdk16
- get: git-repo
- get: paketo-builder-base-image
trigger: true
- get: daily
trigger: true
- do:
- task: run-system-tests
image: ci-image-jdk16
privileged: true
timeout: ((task-timeout))
file: git-repo/ci/tasks/run-system-tests.yml
params:
BRANCH: ((branch))
TOOLCHAIN_JAVA_VERSION: 16
<<: *gradle-enterprise-task-params
<<: *docker-hub-task-params
on_failure:
do:
- put: slack-alert
params:
<<: *slack-fail-params
- put: slack-alert
params:
<<: *slack-success-params
groups:
- name: "builds"
jobs: ["build", "jdk11-build", "jdk16-build", "windows-build"]
- name: "releases"
jobs: ["stage-milestone", "stage-rc", "stage-release", "promote-milestone", "promote-rc", "promote-release", "create-github-release", "publish-gradle-plugin", "publish-to-sdkman", "update-homebrew-tap"]
- name: "system-tests"
jobs: ["run-system-tests", "jdk11-run-system-tests", "jdk16-run-system-tests"]
- name: "ci-images"
jobs: ["build-ci-images", "detect-docker-updates", "detect-jdk-updates", "detect-ubuntu-image-updates"]
- name: "pull-requests"

12
ci/scripts/run-system-tests.sh Executable file
View File

@ -0,0 +1,12 @@
#!/bin/bash
set -e
source $(dirname $0)/common.sh
pushd git-repo > /dev/null
if [[ -d /opt/openjdk-toolchain ]]; then
./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false --no-daemon --max-workers=4 --rerun-tasks systemTest -PtoolchainVersion=${TOOLCHAIN_JAVA_VERSION} -Porg.gradle.java.installations.auto-detect=false -Porg.gradle.java.installations.auto-download=false -Porg.gradle.java.installations.paths=/opt/openjdk-toolchain/
else
./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false --no-daemon --max-workers=4 --rerun-tasks systemTest
fi
popd > /dev/null

View File

@ -0,0 +1,21 @@
---
platform: linux
inputs:
- name: git-repo
caches:
- path: gradle
params:
BRANCH:
CI: true
GRADLE_ENTERPRISE_ACCESS_KEY:
GRADLE_ENTERPRISE_CACHE_USERNAME:
GRADLE_ENTERPRISE_CACHE_PASSWORD:
GRADLE_ENTERPRISE_URL: https://ge.spring.io
run:
path: bash
args:
- -ec
- |
source /docker-lib.sh
start_docker
${PWD}/git-repo/ci/scripts/run-system-tests.sh

View File

@ -75,6 +75,7 @@ include "spring-boot-tests:spring-boot-integration-tests:spring-boot-configurati
include "spring-boot-tests:spring-boot-integration-tests:spring-boot-launch-script-tests"
include "spring-boot-tests:spring-boot-integration-tests:spring-boot-loader-tests"
include "spring-boot-tests:spring-boot-integration-tests:spring-boot-server-tests"
include "spring-boot-system-tests:spring-boot-image-tests"
file("${rootDir}/spring-boot-project/spring-boot-starters").eachDirMatch(~/spring-boot-starter.*/) {
include "spring-boot-project:spring-boot-starters:${it.name}"

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -20,7 +20,7 @@ import org.junit.jupiter.api.TestTemplate;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.gradle.junit.GradleMultiDslExtension;
import org.springframework.boot.gradle.testkit.GradleBuild;
import org.springframework.boot.testsupport.gradle.testkit.GradleBuild;
/**
* Tests for the getting started documentation.

View File

@ -25,7 +25,7 @@ import org.junit.jupiter.api.TestTemplate;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.gradle.junit.GradleMultiDslExtension;
import org.springframework.boot.gradle.testkit.GradleBuild;
import org.springframework.boot.testsupport.gradle.testkit.GradleBuild;
import static org.assertj.core.api.Assertions.assertThat;

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -20,8 +20,8 @@ import org.junit.jupiter.api.TestTemplate;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.gradle.junit.GradleMultiDslExtension;
import org.springframework.boot.gradle.testkit.Dsl;
import org.springframework.boot.gradle.testkit.GradleBuild;
import org.springframework.boot.testsupport.gradle.testkit.Dsl;
import org.springframework.boot.testsupport.gradle.testkit.GradleBuild;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assumptions.assumingThat;

View File

@ -33,7 +33,7 @@ import org.junit.jupiter.api.TestTemplate;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.gradle.junit.GradleMultiDslExtension;
import org.springframework.boot.gradle.testkit.GradleBuild;
import org.springframework.boot.testsupport.gradle.testkit.GradleBuild;
import org.springframework.util.FileCopyUtils;
import static org.assertj.core.api.Assertions.assertThat;

View File

@ -22,7 +22,7 @@ import org.junit.jupiter.api.condition.JRE;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.gradle.junit.GradleMultiDslExtension;
import org.springframework.boot.gradle.testkit.GradleBuild;
import org.springframework.boot.testsupport.gradle.testkit.GradleBuild;
import static org.assertj.core.api.Assertions.assertThat;

View File

@ -27,7 +27,7 @@ import org.junit.jupiter.api.condition.JRE;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.gradle.junit.GradleMultiDslExtension;
import org.springframework.boot.gradle.testkit.GradleBuild;
import org.springframework.boot.testsupport.gradle.testkit.GradleBuild;
import static org.assertj.core.api.Assertions.assertThat;

View File

@ -26,7 +26,7 @@ import org.junit.jupiter.api.TestTemplate;
import org.springframework.boot.gradle.junit.GradleCompatibility;
import org.springframework.boot.gradle.tasks.buildinfo.BuildInfo;
import org.springframework.boot.gradle.testkit.GradleBuild;
import org.springframework.boot.testsupport.gradle.testkit.GradleBuild;
import static org.assertj.core.api.Assertions.assertThat;

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -21,7 +21,7 @@ import java.lang.reflect.Field;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.springframework.boot.gradle.testkit.GradleBuild;
import org.springframework.boot.testsupport.gradle.testkit.GradleBuild;
import org.springframework.util.ReflectionUtils;
/**

View File

@ -26,7 +26,7 @@ import org.junit.jupiter.api.TestTemplate;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.Extension;
import org.springframework.boot.gradle.testkit.GradleBuild;
import org.springframework.boot.testsupport.gradle.testkit.GradleBuild;
/**
* {@link Extension} that runs {@link TestTemplate templated tests} against multiple

View File

@ -21,7 +21,6 @@ import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
import org.gradle.api.JavaVersion;
import org.gradle.util.GradleVersion;
import org.junit.jupiter.api.TestTemplate;
import org.junit.jupiter.api.extension.Extension;
@ -30,8 +29,9 @@ import org.junit.jupiter.api.extension.TestTemplateInvocationContext;
import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider;
import org.junit.platform.commons.util.AnnotationUtils;
import org.springframework.boot.gradle.testkit.GradleBuild;
import org.springframework.boot.gradle.testkit.GradleBuildExtension;
import org.springframework.boot.testsupport.gradle.testkit.GradleBuild;
import org.springframework.boot.testsupport.gradle.testkit.GradleBuildExtension;
import org.springframework.boot.testsupport.gradle.testkit.GradleVersions;
import org.springframework.util.StringUtils;
/**
@ -43,26 +43,11 @@ import org.springframework.util.StringUtils;
*/
final class GradleCompatibilityExtension implements TestTemplateInvocationContextProvider {
private static final List<String> GRADLE_VERSIONS;
static {
JavaVersion javaVersion = JavaVersion.current();
if (javaVersion.isCompatibleWith(JavaVersion.VERSION_16)) {
GRADLE_VERSIONS = Arrays.asList("7.0.2", "7.1.1");
}
else {
GRADLE_VERSIONS = Arrays.asList("6.8.3", "current", "7.0.2", "7.1.1");
}
}
private static final List<String> GRADLE_VERSIONS = GradleVersions.allCompatible();
@Override
public Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContexts(ExtensionContext context) {
Stream<String> gradleVersions = GRADLE_VERSIONS.stream().map((version) -> {
if (version.equals("current")) {
return GradleVersion.current().getVersion();
}
return version;
});
Stream<String> gradleVersions = GRADLE_VERSIONS.stream();
GradleCompatibility gradleCompatibility = AnnotationUtils
.findAnnotation(context.getRequiredTestClass(), GradleCompatibility.class).get();
if (StringUtils.hasText(gradleCompatibility.versionsLessThan())) {

View File

@ -20,16 +20,16 @@ import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
import org.gradle.api.JavaVersion;
import org.junit.jupiter.api.TestTemplate;
import org.junit.jupiter.api.extension.Extension;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.TestTemplateInvocationContext;
import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider;
import org.springframework.boot.gradle.testkit.Dsl;
import org.springframework.boot.gradle.testkit.GradleBuild;
import org.springframework.boot.gradle.testkit.GradleBuildExtension;
import org.springframework.boot.testsupport.gradle.testkit.Dsl;
import org.springframework.boot.testsupport.gradle.testkit.GradleBuild;
import org.springframework.boot.testsupport.gradle.testkit.GradleBuildExtension;
import org.springframework.boot.testsupport.gradle.testkit.GradleVersions;
/**
* {@link Extension} that runs {@link TestTemplate templated tests} against the Groovy and
@ -61,10 +61,7 @@ public class GradleMultiDslExtension implements TestTemplateInvocationContextPro
@Override
public List<Extension> getAdditionalExtensions() {
GradleBuild gradleBuild = new GradleBuild(this.dsl);
JavaVersion javaVersion = JavaVersion.current();
if (javaVersion.isCompatibleWith(JavaVersion.VERSION_16)) {
gradleBuild.gradleVersion("7.0.2");
}
gradleBuild.gradleVersion(GradleVersions.currentOrMinimumCompatible());
return Arrays.asList(new GradleBuildFieldSetter(gradleBuild), new GradleBuildExtension());
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -32,7 +32,7 @@ import org.gradle.testkit.runner.TaskOutcome;
import org.junit.jupiter.api.TestTemplate;
import org.springframework.boot.gradle.junit.GradleCompatibility;
import org.springframework.boot.gradle.testkit.GradleBuild;
import org.springframework.boot.testsupport.gradle.testkit.GradleBuild;
import static org.assertj.core.api.Assertions.assertThat;

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -20,7 +20,7 @@ import org.gradle.testkit.runner.TaskOutcome;
import org.junit.jupiter.api.TestTemplate;
import org.springframework.boot.gradle.junit.GradleCompatibility;
import org.springframework.boot.gradle.testkit.GradleBuild;
import org.springframework.boot.testsupport.gradle.testkit.GradleBuild;
import static org.assertj.core.api.Assertions.assertThat;

View File

@ -26,7 +26,7 @@ import org.gradle.testkit.runner.TaskOutcome;
import org.junit.jupiter.api.TestTemplate;
import org.springframework.boot.gradle.junit.GradleCompatibility;
import org.springframework.boot.gradle.testkit.GradleBuild;
import org.springframework.boot.testsupport.gradle.testkit.GradleBuild;
import static org.assertj.core.api.Assertions.assertThat;

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -19,7 +19,7 @@ package org.springframework.boot.gradle.plugin;
import org.junit.jupiter.api.TestTemplate;
import org.springframework.boot.gradle.junit.GradleCompatibility;
import org.springframework.boot.gradle.testkit.GradleBuild;
import org.springframework.boot.testsupport.gradle.testkit.GradleBuild;
import static org.assertj.core.api.Assertions.assertThat;

View File

@ -21,7 +21,7 @@ import org.junit.jupiter.api.condition.DisabledForJreRange;
import org.junit.jupiter.api.condition.JRE;
import org.springframework.boot.gradle.junit.GradleCompatibility;
import org.springframework.boot.gradle.testkit.GradleBuild;
import org.springframework.boot.testsupport.gradle.testkit.GradleBuild;
import static org.assertj.core.api.Assertions.assertThat;

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -19,7 +19,7 @@ package org.springframework.boot.gradle.plugin;
import org.junit.jupiter.api.TestTemplate;
import org.springframework.boot.gradle.junit.GradleCompatibility;
import org.springframework.boot.gradle.testkit.GradleBuild;
import org.springframework.boot.testsupport.gradle.testkit.GradleBuild;
import static org.assertj.core.api.Assertions.assertThat;

View File

@ -22,8 +22,8 @@ import org.junit.jupiter.api.condition.DisabledForJreRange;
import org.junit.jupiter.api.condition.JRE;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.gradle.testkit.GradleBuild;
import org.springframework.boot.gradle.testkit.GradleBuildExtension;
import org.springframework.boot.testsupport.gradle.testkit.GradleBuild;
import org.springframework.boot.testsupport.gradle.testkit.GradleBuildExtension;
import static org.assertj.core.api.Assertions.assertThat;

View File

@ -23,7 +23,7 @@ import org.gradle.testkit.runner.TaskOutcome;
import org.junit.jupiter.api.TestTemplate;
import org.springframework.boot.gradle.junit.GradleCompatibility;
import org.springframework.boot.gradle.testkit.GradleBuild;
import org.springframework.boot.testsupport.gradle.testkit.GradleBuild;
import static org.assertj.core.api.Assertions.assertThat;

View File

@ -30,8 +30,8 @@ import org.gradle.testkit.runner.TaskOutcome;
import org.junit.jupiter.api.TestTemplate;
import org.springframework.boot.gradle.junit.GradleCompatibility;
import org.springframework.boot.gradle.testkit.GradleBuild;
import org.springframework.boot.loader.tools.FileUtils;
import org.springframework.boot.testsupport.gradle.testkit.GradleBuild;
import static org.assertj.core.api.Assertions.assertThat;

View File

@ -49,9 +49,9 @@ import org.gradle.testkit.runner.BuildResult;
import org.gradle.testkit.runner.TaskOutcome;
import org.junit.jupiter.api.TestTemplate;
import org.springframework.boot.gradle.testkit.GradleBuild;
import org.springframework.boot.loader.tools.FileUtils;
import org.springframework.boot.loader.tools.JarModeLibrary;
import org.springframework.boot.testsupport.gradle.testkit.GradleBuild;
import org.springframework.util.FileSystemUtils;
import org.springframework.util.StringUtils;

View File

@ -44,7 +44,7 @@ import org.springframework.boot.buildpack.platform.docker.type.ImageName;
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
import org.springframework.boot.buildpack.platform.io.FilePermissions;
import org.springframework.boot.gradle.junit.GradleCompatibility;
import org.springframework.boot.gradle.testkit.GradleBuild;
import org.springframework.boot.testsupport.gradle.testkit.GradleBuild;
import org.springframework.boot.testsupport.testcontainers.DisabledIfDockerUnavailable;
import static org.assertj.core.api.Assertions.assertThat;

View File

@ -36,7 +36,7 @@ import org.springframework.boot.buildpack.platform.docker.UpdateListener;
import org.springframework.boot.buildpack.platform.docker.type.Image;
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
import org.springframework.boot.gradle.junit.GradleCompatibility;
import org.springframework.boot.gradle.testkit.GradleBuild;
import org.springframework.boot.testsupport.gradle.testkit.GradleBuild;
import org.springframework.boot.testsupport.testcontainers.DockerImageNames;
import static org.assertj.core.api.Assertions.assertThat;

View File

@ -25,7 +25,7 @@ import org.junit.jupiter.api.condition.DisabledForJreRange;
import org.junit.jupiter.api.condition.JRE;
import org.springframework.boot.gradle.junit.GradleCompatibility;
import org.springframework.boot.gradle.testkit.GradleBuild;
import org.springframework.boot.testsupport.gradle.testkit.GradleBuild;
import static org.assertj.core.api.Assertions.assertThat;

View File

@ -23,7 +23,7 @@ import org.gradle.testkit.runner.TaskOutcome;
import org.junit.jupiter.api.TestTemplate;
import org.springframework.boot.gradle.junit.GradleCompatibility;
import org.springframework.boot.gradle.testkit.GradleBuild;
import org.springframework.boot.testsupport.gradle.testkit.GradleBuild;
import static org.assertj.core.api.Assertions.assertThat;

View File

@ -30,7 +30,7 @@ import org.gradle.testkit.runner.TaskOutcome;
import org.junit.jupiter.api.TestTemplate;
import org.springframework.boot.gradle.junit.GradleCompatibility;
import org.springframework.boot.gradle.testkit.GradleBuild;
import org.springframework.boot.testsupport.gradle.testkit.GradleBuild;
import org.springframework.util.FileSystemUtils;
import static org.assertj.core.api.Assertions.assertThat;

View File

@ -20,6 +20,16 @@ dependencies {
compileOnly("org.springframework.data:spring-data-redis")
compileOnly("org.testcontainers:testcontainers")
compileOnly(project(":spring-boot-project:spring-boot-tools:spring-boot-buildpack-platform"))
compileOnly(project(":spring-boot-project:spring-boot-tools:spring-boot-loader-tools"))
compileOnly("io.spring.gradle:dependency-management-plugin")
compileOnly(gradleTestKit())
compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
compileOnly("org.jetbrains.kotlin:kotlin-compiler-embeddable:$kotlinVersion")
compileOnly("org.jetbrains.kotlin:kotlin-compiler-runner:$kotlinVersion")
compileOnly("org.jetbrains.kotlin:kotlin-daemon-client:$kotlinVersion")
compileOnly("org.apache.commons:commons-compress")
implementation("org.apache.maven.resolver:maven-resolver-connector-basic")
implementation("org.apache.maven.resolver:maven-resolver-impl")
implementation("org.apache.maven:maven-resolver-provider")

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -14,13 +14,18 @@
* limitations under the License.
*/
package org.springframework.boot.gradle.testkit;
package org.springframework.boot.testsupport.gradle.testkit;
/**
* The DSLs supported by Gradle and demonstrated in the documentation samples
* The DSLs supported by Gradle and demonstrated in the documentation samples.
*
* @author Andy Wilkinson
*/
public enum Dsl {
/**
* Supported DSL variants.
*/
GROOVY("Groovy", ".gradle"), KOTLIN("Kotlin", ".gradle.kts");
private final String name;

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.gradle.testkit;
package org.springframework.boot.testsupport.gradle.testkit;
import java.io.File;
import java.io.FileReader;
@ -74,8 +74,12 @@ public class GradleBuild {
private String script;
private String settings;
private String gradleVersion;
private String springBootVersion = "TEST-SNAPSHOT";
private GradleVersion expectDeprecationWarnings;
private boolean configurationCache = false;
@ -84,8 +88,6 @@ public class GradleBuild {
public GradleBuild() {
this(Dsl.GROOVY);
this.scriptProperties.put("bootVersion", getBootVersion());
this.scriptProperties.put("dependencyManagementPluginVersion", getDependencyManagementPluginVersion());
}
public GradleBuild(Dsl dsl) {
@ -133,6 +135,10 @@ public class GradleBuild {
return this;
}
public void settings(String settings) {
this.settings = settings;
}
public GradleBuild expectDeprecationWarningsWithAtLeastVersion(String gradleVersion) {
this.expectDeprecationWarnings = GradleVersion.version(gradleVersion);
return this;
@ -173,12 +179,20 @@ public class GradleBuild {
public GradleRunner prepareRunner(String... arguments) throws IOException {
String scriptContent = FileCopyUtils.copyToString(new FileReader(this.script));
this.scriptProperties.put("bootVersion", getBootVersion());
this.scriptProperties.put("dependencyManagementPluginVersion", getDependencyManagementPluginVersion());
for (Entry<String, String> property : this.scriptProperties.entrySet()) {
scriptContent = scriptContent.replace("{" + property.getKey() + "}", property.getValue());
}
FileCopyUtils.copy(scriptContent, new FileWriter(new File(this.projectDir, "build" + this.dsl.getExtension())));
FileSystemUtils.copyRecursively(new File("src/test/resources/repository"),
new File(this.projectDir, "repository"));
if (this.settings != null) {
FileCopyUtils.copy(new FileReader(this.settings),
new FileWriter(new File(this.projectDir, "settings.gradle")));
}
File repository = new File("src/test/resources/repository");
if (repository.exists()) {
FileSystemUtils.copyRecursively(repository, new File(this.projectDir, "repository"));
}
GradleRunner gradleRunner = GradleRunner.create().withProjectDir(this.projectDir)
.withPluginClasspath(pluginClasspath());
if (this.dsl != Dsl.KOTLIN && !this.configurationCache) {
@ -225,8 +239,13 @@ public class GradleBuild {
return this.gradleVersion;
}
private static String getBootVersion() {
return "TEST-SNAPSHOT";
public GradleBuild bootVersion(String version) {
this.springBootVersion = version;
return this;
}
private String getBootVersion() {
return this.springBootVersion;
}
private static String getDependencyManagementPluginVersion() {

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.gradle.testkit;
package org.springframework.boot.testsupport.gradle.testkit;
import java.lang.reflect.Field;
import java.net.URL;
@ -32,6 +32,7 @@ import org.springframework.util.ReflectionUtils;
* field named {@code gradleBuild}.
*
* @author Andy Wilkinson
* @author Scott Frederick
*/
public class GradleBuildExtension implements BeforeEachCallback, AfterEachCallback {
@ -46,6 +47,10 @@ public class GradleBuildExtension implements BeforeEachCallback, AfterEachCallba
if (scriptUrl != null) {
gradleBuild.script(scriptUrl.getFile());
}
URL settingsUrl = getSettings(context);
if (settingsUrl != null) {
gradleBuild.settings(settingsUrl.getFile());
}
gradleBuild.before();
}
@ -80,6 +85,11 @@ public class GradleBuildExtension implements BeforeEachCallback, AfterEachCallba
return testClass.getResource(testClass.getSimpleName() + this.dsl.getExtension());
}
private URL getSettings(ExtensionContext context) {
Class<?> testClass = context.getRequiredTestClass();
return testClass.getResource("settings.gradle");
}
@Override
public void afterEach(ExtensionContext context) throws Exception {
extractGradleBuild(context).after();

View File

@ -0,0 +1,53 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* 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
*
* https://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 org.springframework.boot.testsupport.gradle.testkit;
import java.util.Arrays;
import java.util.List;
import org.gradle.api.JavaVersion;
import org.gradle.util.GradleVersion;
/**
* Versions of Gradle used for testing.
*
* @author Scott Frederick
*/
public final class GradleVersions {
private GradleVersions() {
}
public static List<String> allCompatible() {
if (isJava16()) {
return Arrays.asList("7.0.2", "7.1");
}
return Arrays.asList("6.8.3", GradleVersion.current().getVersion(), "7.0.2", "7.1.1");
}
public static String currentOrMinimumCompatible() {
if (isJava16()) {
return "7.0.2";
}
return GradleVersion.current().getVersion();
}
private static boolean isJava16() {
return JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_16);
}
}

View File

@ -0,0 +1,35 @@
plugins {
id 'java-gradle-plugin'
id "org.springframework.boot.conventions"
id "org.springframework.boot.system-test"
}
description = "Spring Boot Image Building Tests"
configurations {
providedRuntime {
extendsFrom dependencyManagement
}
}
systemTest {
if (project.hasProperty("springBootVersion")) {
systemProperty "springBootVersion", project.properties["springBootVersion"]
} else {
systemProperty "springBootVersion", project.getVersion()
}
}
dependencies {
implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-web")) {
exclude group: "org.hibernate.validator"
}
systemTestImplementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-test"))
systemTestImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support"))
systemTestImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-buildpack-platform"))
systemTestImplementation(gradleTestKit())
systemTestImplementation("org.assertj:assertj-core")
systemTestImplementation("org.testcontainers:junit-jupiter")
systemTestImplementation("org.testcontainers:testcontainers")
}

View File

@ -0,0 +1,134 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* 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
*
* https://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 org.springframework.boot.image.assertions;
import java.util.List;
import java.util.Map;
import com.github.dockerjava.api.model.ContainerConfig;
import org.assertj.core.api.AbstractAssert;
import org.assertj.core.api.AbstractListAssert;
import org.assertj.core.api.AbstractObjectAssert;
import org.assertj.core.api.AbstractStringAssert;
import org.assertj.core.api.Assertions;
import org.assertj.core.api.AssertionsForClassTypes;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.assertj.core.api.ListAssert;
import org.assertj.core.api.ObjectAssert;
import org.springframework.boot.test.json.JsonContentAssert;
/**
* AssertJ {@link org.assertj.core.api.Assert} for Docker image container configuration.
*
* @author Scott Frederick
*/
public class ContainerConfigAssert extends AbstractAssert<ContainerConfigAssert, ContainerConfig> {
private static final String BUILD_METADATA_LABEL = "io.buildpacks.build.metadata";
private static final String LIFECYCLE_METADATA_LABEL = "io.buildpacks.lifecycle.metadata";
ContainerConfigAssert(ContainerConfig containerConfig) {
super(containerConfig, ContainerConfigAssert.class);
}
public BuildMetadataAssert buildMetadata() {
return new BuildMetadataAssert(jsonLabel(BUILD_METADATA_LABEL));
}
public LifecycleMetadataAssert lifecycleMetadata() {
return new LifecycleMetadataAssert(jsonLabel(LIFECYCLE_METADATA_LABEL));
}
public AbstractStringAssert<?> label(String label) {
return AssertionsForClassTypes.assertThat(getLabel(label));
}
private JsonContentAssert jsonLabel(String label) {
return new JsonContentAssert(ContainerConfigAssert.class, getLabel(label));
}
private String getLabel(String label) {
Map<String, String> labels = this.actual.getLabels();
if (labels == null) {
failWithMessage("Container config contains no labels");
}
if (!labels.containsKey(label)) {
failWithActualExpectedAndMessage(labels, label, "Expected label not found in container config");
}
return labels.get(label);
}
/**
* Asserts for the JSON content in the {@code io.buildpacks.build.metadata} label.
*
* See <a href=
* "https://github.com/buildpacks/spec/blob/main/platform.md#iobuildpacksbuildmetadata-json">the
* spec</a>
*/
public static class BuildMetadataAssert extends AbstractAssert<BuildMetadataAssert, JsonContentAssert> {
BuildMetadataAssert(JsonContentAssert jsonContentAssert) {
super(jsonContentAssert, BuildMetadataAssert.class);
}
public ListAssert<Object> buildpacks() {
return this.actual.extractingJsonPathArrayValue("$.buildpacks[*].id");
}
public ListAssert<Object> bomDependencies() {
return this.actual
.extractingJsonPathArrayValue("$.bom[?(@.name=='dependencies')].metadata.dependencies[*].name");
}
public AbstractStringAssert<?> bomJavaVersion(String javaType) {
return this.actual.extractingJsonPathArrayValue("$.bom[?(@.name=='%s')].metadata.version", javaType)
.singleElement(Assertions.as(InstanceOfAssertFactories.STRING));
}
public AbstractObjectAssert<?, Object> processOfType(String type) {
return this.actual.extractingJsonPathArrayValue("$.processes[?(@.type=='%s')]", type).singleElement();
}
}
/**
* Asserts for the the JSON content in the {@code io.buildpacks.lifecycle.metadata}
* label.
*
* See <a href=
* "https://github.com/buildpacks/spec/blob/main/platform.md#iobuildpackslifecyclemetadata-json">the
* spec</a>
*/
public static class LifecycleMetadataAssert extends AbstractAssert<LifecycleMetadataAssert, JsonContentAssert> {
LifecycleMetadataAssert(JsonContentAssert jsonContentAssert) {
super(jsonContentAssert, LifecycleMetadataAssert.class);
}
public ListAssert<Object> buildpackLayers(String buildpackId) {
return this.actual.extractingJsonPathArrayValue("$.buildpacks[?(@.key=='%s')].layers", buildpackId);
}
public AbstractListAssert<?, List<?>, Object, ObjectAssert<Object>> appLayerShas() {
return this.actual.extractingJsonPathArrayValue("$.app").extracting("sha");
}
}
}

View File

@ -0,0 +1,91 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* 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
*
* https://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 org.springframework.boot.image.assertions;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.assertj.core.api.AbstractAssert;
import org.assertj.core.api.Assertions;
import org.assertj.core.api.ListAssert;
import org.springframework.boot.buildpack.platform.docker.DockerApi;
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
import org.springframework.boot.buildpack.platform.docker.type.Layer;
/**
* AssertJ {@link org.assertj.core.api.Assert} for Docker image contents.
*
* @author Scott Frederick
*/
public class ImageAssert extends AbstractAssert<ImageAssert, ImageReference> {
private final HashMap<String, Layer> layers = new HashMap<>();
ImageAssert(ImageReference imageReference) throws IOException {
super(imageReference, ImageAssert.class);
getLayers();
}
public LayerContentAssert hasLayer(String layerDigest) {
if (!this.layers.containsKey(layerDigest)) {
failWithMessage("Layer with digest '%s' not found in image", layerDigest);
}
return new LayerContentAssert(this.layers.get(layerDigest));
}
private void getLayers() throws IOException {
new DockerApi().image().exportLayers(this.actual, (id, tarArchive) -> {
Layer layer = Layer.fromTarArchive(tarArchive);
this.layers.put(layer.getId().toString(), layer);
});
}
/**
* Asserts for image layers.
*/
public static class LayerContentAssert extends AbstractAssert<LayerContentAssert, Layer> {
public LayerContentAssert(Layer layer) {
super(layer, LayerContentAssert.class);
}
public ListAssert<String> entries() throws IOException {
List<String> entryNames = new ArrayList<>();
ByteArrayOutputStream out = new ByteArrayOutputStream();
this.actual.writeTo(out);
try (TarArchiveInputStream in = new TarArchiveInputStream(new ByteArrayInputStream(out.toByteArray()))) {
TarArchiveEntry entry = in.getNextTarEntry();
while (entry != null) {
if (!entry.isDirectory()) {
entryNames.add(entry.getName().replaceFirst("^/workspace/", ""));
}
entry = in.getNextTarEntry();
}
}
return Assertions.assertThat(entryNames);
}
}
}

View File

@ -0,0 +1,44 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* 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
*
* https://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 org.springframework.boot.image.assertions;
import java.io.IOException;
import com.github.dockerjava.api.model.ContainerConfig;
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
/**
* Factory class for custom AssertJ {@link org.assertj.core.api.Assert}s related to images
* and containers.
*
* @author Scott Frederick
*/
public final class ImageAssertions {
private ImageAssertions() {
}
public static ContainerConfigAssert assertThat(ContainerConfig containerConfig) {
return new ContainerConfigAssert(containerConfig);
}
public static ImageAssert assertThat(ImageReference imageReference) throws IOException {
return new ImageAssert(imageReference);
}
}

View File

@ -0,0 +1,54 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* 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
*
* https://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 org.springframework.boot.image.junit;
import java.lang.reflect.Field;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.springframework.boot.testsupport.gradle.testkit.GradleBuild;
import org.springframework.boot.testsupport.gradle.testkit.GradleVersions;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
/**
* A {@link BeforeEachCallback} to configure and set a test class's {@code gradleBuild}
* field prior to test execution.
*
* @author Scott Frederick
*/
public class GradleBuildInjectionExtension implements BeforeEachCallback {
private final GradleBuild gradleBuild;
GradleBuildInjectionExtension() {
this.gradleBuild = new GradleBuild();
this.gradleBuild.gradleVersion(GradleVersions.currentOrMinimumCompatible());
String bootVersion = System.getProperty("springBootVersion");
Assert.notNull(bootVersion, "Property 'springBootVersion' must be set in build environment");
this.gradleBuild.bootVersion(bootVersion);
}
@Override
public void beforeEach(ExtensionContext context) throws Exception {
Field field = ReflectionUtils.findField(context.getRequiredTestClass(), "gradleBuild");
field.setAccessible(true);
field.set(context.getRequiredTestInstance(), this.gradleBuild);
}
}

View File

@ -0,0 +1,53 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* 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
*
* https://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 org.springframework.boot.image.paketo;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.Constructor;
/**
* Index file describing the layers in the jar or war file and the files or directories in
* each layer.
*
* @author Scott Frederick
*/
class LayersIndex extends ArrayList<Map<String, List<String>>> {
List<String> getLayer(String layerName) {
return stream().filter((entry) -> entry.containsKey(layerName)).findFirst().map((entry) -> entry.get(layerName))
.orElse(Collections.emptyList());
}
static LayersIndex fromArchiveFile(File archiveFile) throws IOException {
String indexPath = (archiveFile.getName().endsWith(".war") ? "WEB-INF/layers.idx" : "BOOT-INF/layers.idx");
try (JarFile jarFile = new JarFile(archiveFile)) {
ZipEntry indexEntry = jarFile.getEntry(indexPath);
Yaml yaml = new Yaml(new Constructor(LayersIndex.class));
return yaml.load(jarFile.getInputStream(indexEntry));
}
}
}

View File

@ -0,0 +1,402 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* 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
*
* https://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 org.springframework.boot.image.paketo;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import com.github.dockerjava.api.model.ContainerConfig;
import org.assertj.core.api.Condition;
import org.gradle.testkit.runner.BuildResult;
import org.gradle.testkit.runner.TaskOutcome;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.wait.strategy.Wait;
import org.springframework.boot.buildpack.platform.docker.DockerApi;
import org.springframework.boot.buildpack.platform.docker.type.ImageName;
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
import org.springframework.boot.testsupport.gradle.testkit.GradleBuild;
import org.springframework.boot.testsupport.gradle.testkit.GradleBuildExtension;
import org.springframework.boot.image.assertions.ImageAssertions;
import org.springframework.boot.image.junit.GradleBuildInjectionExtension;
import org.springframework.util.StringUtils;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for the Paketo builder and buildpacks.
*
* See
* https://paketo.io/docs/buildpacks/language-family-buildpacks/java/#additional-metadata
*
* @author Scott Frederick
*/
@ExtendWith({ GradleBuildInjectionExtension.class, GradleBuildExtension.class })
class PaketoBuilderTests {
GradleBuild gradleBuild;
@Test
void executableJarApp() throws Exception {
writeMainClass();
String imageName = "paketo-integration/" + this.gradleBuild.getProjectDir().getName();
ImageReference imageReference = ImageReference.of(ImageName.of(imageName));
BuildResult result = buildImage(imageName);
assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
try (GenericContainer<?> container = new GenericContainer<>(imageName).withExposedPorts(8080)) {
container.waitingFor(Wait.forHttp("/test")).start();
ContainerConfig config = container.getContainerInfo().getConfig();
assertLabelsMatchManifestAttributes(config);
ImageAssertions.assertThat(config).buildMetadata().buildpacks().containsExactly(
"paketo-buildpacks/ca-certificates", "paketo-buildpacks/bellsoft-liberica",
"paketo-buildpacks/executable-jar", "paketo-buildpacks/dist-zip", "paketo-buildpacks/spring-boot");
ImageAssertions.assertThat(config).buildMetadata().bomDependencies().contains("spring-beans", "spring-boot",
"spring-boot-autoconfigure", "spring-boot-jarmode-layertools", "spring-context", "spring-core",
"spring-web");
ImageAssertions.assertThat(config).buildMetadata().bomJavaVersion("jre").startsWith(javaMajorVersion());
ImageAssertions.assertThat(config).buildMetadata().processOfType("web").extracting("command", "args")
.containsExactly("java", Collections.singletonList("org.springframework.boot.loader.JarLauncher"));
ImageAssertions.assertThat(config).buildMetadata().processOfType("executable-jar")
.extracting("command", "args")
.containsExactly("java", Collections.singletonList("org.springframework.boot.loader.JarLauncher"));
assertImageLayersMatchLayersIndex(imageReference, config);
}
finally {
removeImage(imageReference);
}
}
@Test
void executableJarAppWithAdditionalArgs() throws Exception {
writeMainClass();
String imageName = "paketo-integration/" + this.gradleBuild.getProjectDir().getName();
ImageReference imageReference = ImageReference.of(ImageName.of(imageName));
BuildResult result = buildImage(imageName);
assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
try (GenericContainer<?> container = new GenericContainer<>(imageName).withCommand("--server.port=9090")
.withExposedPorts(9090)) {
container.waitingFor(Wait.forHttp("/test")).start();
}
finally {
removeImage(imageReference);
}
}
@Test
void executableJarAppBuiltTwiceWithCaching() throws Exception {
writeMainClass();
String imageName = "paketo-integration/" + this.gradleBuild.getProjectDir().getName();
ImageReference imageReference = ImageReference.of(ImageName.of(imageName));
BuildResult result = buildImage(imageName);
assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
try (GenericContainer<?> container = new GenericContainer<>(imageName).withExposedPorts(8080)) {
container.waitingFor(Wait.forHttp("/test")).start();
container.stop();
}
result = buildImage(imageName);
assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
try (GenericContainer<?> container = new GenericContainer<>(imageName).withExposedPorts(8080)) {
container.waitingFor(Wait.forHttp("/test")).start();
}
finally {
removeImage(imageReference);
}
}
@Test
void bootDistZipJarApp() throws Exception {
writeMainClass();
String projectName = this.gradleBuild.getProjectDir().getName();
String imageName = "paketo-integration/" + projectName;
ImageReference imageReference = ImageReference.of(ImageName.of(imageName));
BuildResult result = buildImage(imageName, "assemble", "bootDistZip");
assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
try (GenericContainer<?> container = new GenericContainer<>(imageName).withExposedPorts(8080)) {
container.waitingFor(Wait.forHttp("/test")).start();
ContainerConfig config = container.getContainerInfo().getConfig();
ImageAssertions.assertThat(config).buildMetadata().buildpacks().containsExactly(
"paketo-buildpacks/ca-certificates", "paketo-buildpacks/bellsoft-liberica",
"paketo-buildpacks/dist-zip", "paketo-buildpacks/spring-boot");
ImageAssertions.assertThat(config).buildMetadata().bomJavaVersion("jre").startsWith(javaMajorVersion());
ImageAssertions.assertThat(config).buildMetadata().processOfType("web").extracting("command", "args")
.containsExactly("/workspace/" + projectName + "-boot/bin/" + projectName, Collections.emptyList());
ImageAssertions.assertThat(config).buildMetadata().processOfType("dist-zip").extracting("command", "args")
.containsExactly("/workspace/" + projectName + "-boot/bin/" + projectName, Collections.emptyList());
DigestCapturingCondition digests = new DigestCapturingCondition();
ImageAssertions.assertThat(config).lifecycleMetadata().appLayerShas().haveExactly(1, digests);
ImageAssertions.assertThat(imageReference).hasLayer(digests.getDigest(0)).entries().contains(
projectName + "-boot/bin/" + projectName, projectName + "-boot/lib/" + projectName + ".jar");
}
finally {
removeImage(imageReference);
}
}
@Test
void plainDistZipJarApp() throws Exception {
writeMainClass();
String projectName = this.gradleBuild.getProjectDir().getName();
String imageName = "paketo-integration/" + projectName;
ImageReference imageReference = ImageReference.of(ImageName.of(imageName));
BuildResult result = buildImage(imageName, "assemble", "bootDistZip");
assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
try (GenericContainer<?> container = new GenericContainer<>(imageName).withExposedPorts(8080)) {
container.waitingFor(Wait.forHttp("/test")).start();
ContainerConfig config = container.getContainerInfo().getConfig();
ImageAssertions.assertThat(config).buildMetadata().buildpacks().containsExactly(
"paketo-buildpacks/ca-certificates", "paketo-buildpacks/bellsoft-liberica",
"paketo-buildpacks/dist-zip", "paketo-buildpacks/spring-boot");
ImageAssertions.assertThat(config).buildMetadata().bomJavaVersion("jre").startsWith(javaMajorVersion());
ImageAssertions.assertThat(config).buildMetadata().processOfType("web").extracting("command", "args")
.containsExactly("/workspace/" + projectName + "/bin/" + projectName, Collections.emptyList());
ImageAssertions.assertThat(config).buildMetadata().processOfType("dist-zip").extracting("command", "args")
.containsExactly("/workspace/" + projectName + "/bin/" + projectName, Collections.emptyList());
DigestCapturingCondition digests = new DigestCapturingCondition();
ImageAssertions.assertThat(config).lifecycleMetadata().appLayerShas().haveExactly(1, digests);
ImageAssertions.assertThat(imageReference).hasLayer(digests.getDigest(0)).entries()
.contains(projectName + "/bin/" + projectName, projectName + "/lib/" + projectName + "-plain.jar")
.anyMatch((s) -> s.startsWith(projectName + "/lib/spring-boot-"))
.anyMatch((s) -> s.startsWith(projectName + "/lib/spring-core-"))
.anyMatch((s) -> s.startsWith(projectName + "/lib/spring-web-"));
}
finally {
removeImage(imageReference);
}
}
@Test
void executableWarApp() throws Exception {
writeMainClass();
writeServletInitializerClass();
String imageName = "paketo-integration/" + this.gradleBuild.getProjectDir().getName();
ImageReference imageReference = ImageReference.of(ImageName.of(imageName));
BuildResult result = buildImage(imageName);
assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
try (GenericContainer<?> container = new GenericContainer<>(imageName).withExposedPorts(8080)) {
container.waitingFor(Wait.forHttp("/test")).start();
ContainerConfig config = container.getContainerInfo().getConfig();
assertLabelsMatchManifestAttributes(config);
ImageAssertions.assertThat(config).buildMetadata().buildpacks().containsExactly(
"paketo-buildpacks/ca-certificates", "paketo-buildpacks/bellsoft-liberica",
"paketo-buildpacks/executable-jar", "paketo-buildpacks/dist-zip", "paketo-buildpacks/spring-boot");
ImageAssertions.assertThat(config).buildMetadata().bomDependencies().contains("spring-beans", "spring-boot",
"spring-boot-autoconfigure", "spring-boot-jarmode-layertools", "spring-context", "spring-core",
"spring-web");
ImageAssertions.assertThat(config).buildMetadata().bomJavaVersion("jre").startsWith(javaMajorVersion());
ImageAssertions.assertThat(config).buildMetadata().processOfType("web").extracting("command", "args")
.containsExactly("java", Collections.singletonList("org.springframework.boot.loader.WarLauncher"));
ImageAssertions.assertThat(config).buildMetadata().processOfType("executable-jar")
.extracting("command", "args")
.containsExactly("java", Collections.singletonList("org.springframework.boot.loader.WarLauncher"));
assertImageLayersMatchLayersIndex(imageReference, config);
}
finally {
removeImage(imageReference);
}
}
@Test
void plainWarApp() throws Exception {
writeMainClass();
writeServletInitializerClass();
String imageName = "paketo-integration/" + this.gradleBuild.getProjectDir().getName();
ImageReference imageReference = ImageReference.of(ImageName.of(imageName));
BuildResult result = buildImage(imageName);
assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
try (GenericContainer<?> container = new GenericContainer<>(imageName).withExposedPorts(8080)) {
container.waitingFor(Wait.forHttp("/test")).start();
ContainerConfig config = container.getContainerInfo().getConfig();
ImageAssertions.assertThat(config).buildMetadata().buildpacks().containsExactly(
"paketo-buildpacks/ca-certificates", "paketo-buildpacks/bellsoft-liberica",
"paketo-buildpacks/apache-tomcat", "paketo-buildpacks/dist-zip", "paketo-buildpacks/spring-boot");
ImageAssertions.assertThat(config).buildMetadata().bomJavaVersion("jre").startsWith(javaMajorVersion());
ImageAssertions.assertThat(config).buildMetadata().processOfType("web").extracting("command", "args")
.containsExactly("catalina.sh", Collections.singletonList("run"));
ImageAssertions.assertThat(config).buildMetadata().processOfType("tomcat").extracting("command", "args")
.containsExactly("catalina.sh", Collections.singletonList("run"));
DigestCapturingCondition digests = new DigestCapturingCondition();
ImageAssertions.assertThat(config).lifecycleMetadata().appLayerShas().haveExactly(1, digests);
ImageAssertions.assertThat(imageReference).hasLayer(digests.getDigest(0)).entries()
.contains("WEB-INF/classes/example/ExampleApplication.class",
"WEB-INF/classes/example/HelloController.class", "META-INF/MANIFEST.MF")
.anyMatch((s) -> s.startsWith("WEB-INF/lib/spring-boot-"))
.anyMatch((s) -> s.startsWith("WEB-INF/lib/spring-core-"))
.anyMatch((s) -> s.startsWith("WEB-INF/lib/spring-web-"));
}
finally {
removeImage(imageReference);
}
}
public BuildResult buildImage(String imageName, String... arguments) {
String[] buildImageArgs = { "bootBuildImage", "--imageName=" + imageName, "--pullPolicy=IF_NOT_PRESENT" };
String[] args = StringUtils.concatenateStringArrays(arguments, buildImageArgs);
return this.gradleBuild.build(args);
}
private void writeMainClass() throws IOException {
writeProjectFile("ExampleApplication.java", (writer) -> {
writer.println("package example;");
writer.println();
writer.println("import org.springframework.boot.SpringApplication;");
writer.println("import org.springframework.boot.autoconfigure.SpringBootApplication;");
writer.println("import org.springframework.stereotype.Controller;");
writer.println("import org.springframework.web.bind.annotation.RequestMapping;");
writer.println("import org.springframework.web.bind.annotation.ResponseBody;");
writer.println();
writer.println("@SpringBootApplication");
writer.println("public class ExampleApplication {");
writer.println();
writer.println(" public static void main(String[] args) {");
writer.println(" SpringApplication.run(ExampleApplication.class, args);");
writer.println(" }");
writer.println();
writer.println("}");
writer.println();
writer.println("@Controller");
writer.println("class HelloController {");
writer.println();
writer.println(" @RequestMapping(\"/test\")");
writer.println(" @ResponseBody");
writer.println(" String home() {");
writer.println(" return \"Hello, world!\";");
writer.println(" }");
writer.println();
writer.println("}");
});
}
private void writeServletInitializerClass() throws IOException {
writeProjectFile("ServletInitializer.java", (writer) -> {
writer.println("package example;");
writer.println();
writer.println("import org.springframework.boot.builder.SpringApplicationBuilder;");
writer.println("import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;");
writer.println();
writer.println("public class ServletInitializer extends SpringBootServletInitializer {");
writer.println();
writer.println(" @Override");
writer.println(" protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {");
writer.println(" return application.sources(ExampleApplication.class);");
writer.println(" }");
writer.println();
writer.println("}");
});
}
private void writeProjectFile(String fileName, Consumer<PrintWriter> consumer) throws IOException {
File examplePackage = new File(this.gradleBuild.getProjectDir(), "src/main/java/example");
examplePackage.mkdirs();
File main = new File(examplePackage, fileName);
try (PrintWriter writer = new PrintWriter(new FileWriter(main))) {
consumer.accept(writer);
}
}
private void assertLabelsMatchManifestAttributes(ContainerConfig config) throws IOException {
JarFile jarFile = new JarFile(projectArchiveFile());
Attributes attributes = jarFile.getManifest().getMainAttributes();
ImageAssertions.assertThat(config).label("org.springframework.boot.version")
.isEqualTo(attributes.getValue("Spring-Boot-Version"));
ImageAssertions.assertThat(config).label("org.opencontainers.image.title")
.isEqualTo(attributes.getValue(Attributes.Name.IMPLEMENTATION_TITLE));
ImageAssertions.assertThat(config).label("org.opencontainers.image.version")
.isEqualTo(attributes.getValue(Attributes.Name.IMPLEMENTATION_VERSION));
}
private void assertImageLayersMatchLayersIndex(ImageReference imageReference, ContainerConfig config)
throws IOException {
DigestCapturingCondition digests = new DigestCapturingCondition();
ImageAssertions.assertThat(config).lifecycleMetadata().appLayerShas().haveExactly(5, digests);
LayersIndex layersIndex = LayersIndex.fromArchiveFile(projectArchiveFile());
ImageAssertions.assertThat(imageReference).hasLayer(digests.getDigest(0)).entries()
.allMatch((entry) -> startsWithOneOf(entry, layersIndex.getLayer("dependencies")));
ImageAssertions.assertThat(imageReference).hasLayer(digests.getDigest(1)).entries()
.allMatch((entry) -> startsWithOneOf(entry, layersIndex.getLayer("spring-boot-loader")));
ImageAssertions.assertThat(imageReference).hasLayer(digests.getDigest(2)).entries()
.allMatch((entry) -> startsWithOneOf(entry, layersIndex.getLayer("snapshot-dependencies")));
ImageAssertions.assertThat(imageReference).hasLayer(digests.getDigest(3)).entries()
.allMatch((entry) -> startsWithOneOf(entry, layersIndex.getLayer("application")));
ImageAssertions.assertThat(imageReference).hasLayer(digests.getDigest(4)).entries()
.allMatch((entry) -> entry.contains("lib/spring-cloud-bindings-"));
}
private File projectArchiveFile() {
return new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0];
}
private String javaMajorVersion() {
String javaVersion = System.getProperty("java.version");
if (javaVersion.startsWith("1.")) {
return javaVersion.substring(2, 3);
}
else {
int firstDotIndex = javaVersion.indexOf(".");
if (firstDotIndex != -1) {
return javaVersion.substring(0, firstDotIndex);
}
}
return javaVersion;
}
private boolean startsWithOneOf(String actual, List<String> expectedPrefixes) {
for (String prefix : expectedPrefixes) {
if (actual.startsWith(prefix)) {
return true;
}
}
return false;
}
private void removeImage(ImageReference image) throws IOException {
new DockerApi().image().remove(image, false);
}
private static class DigestCapturingCondition extends Condition<Object> {
private static List<String> digests;
DigestCapturingCondition() {
super(predicate(), "a value starting with 'sha256:'");
}
private static Predicate<Object> predicate() {
digests = new ArrayList<>();
return (sha) -> {
digests.add(sha.toString());
return sha.toString().startsWith("sha256:");
};
}
String getDigest(int index) {
return digests.get(index);
}
}
}

View File

@ -0,0 +1,33 @@
plugins {
id 'org.springframework.boot' version '{bootVersion}'
id 'io.spring.dependency-management' version '{dependencyManagementPluginVersion}'
id 'java'
id 'application'
}
repositories {
mavenCentral()
maven { url 'https://repo.spring.io/milestone' }
maven { url 'https://repo.spring.io/snapshot' }
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web:{bootVersion}")
}
bootJar {
manifest {
attributes(
'Implementation-Version': '1.0.0',
'Implementation-Title': "Paketo Test"
)
}
}
application {
mainClass = 'example.ExampleApplication'
}
bootBuildImage {
archiveFile = new File("${buildDir}/distributions/${project.name}-boot.zip")
}

View File

@ -0,0 +1,26 @@
plugins {
id 'org.springframework.boot' version '{bootVersion}'
id 'io.spring.dependency-management' version '{dependencyManagementPluginVersion}'
id 'java'
id 'war'
}
repositories {
mavenCentral()
maven { url 'https://repo.spring.io/milestone' }
maven { url 'https://repo.spring.io/snapshot' }
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web:{bootVersion}")
providedRuntime("org.springframework.boot:spring-boot-starter-tomcat:{bootVersion}")
}
bootWar {
manifest {
attributes(
'Implementation-Version': '1.0.0',
'Implementation-Title': "Paketo Test"
)
}
}

View File

@ -0,0 +1,33 @@
plugins {
id 'org.springframework.boot' version '{bootVersion}'
id 'io.spring.dependency-management' version '{dependencyManagementPluginVersion}'
id 'java'
id 'application'
}
repositories {
mavenCentral()
maven { url 'https://repo.spring.io/milestone' }
maven { url 'https://repo.spring.io/snapshot' }
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web:{bootVersion}")
}
bootJar {
manifest {
attributes(
'Implementation-Version': '1.0.0',
'Implementation-Title': "Paketo Test"
)
}
}
application {
mainClass = 'example.ExampleApplication'
}
bootBuildImage {
archiveFile = new File("${buildDir}/distributions/${project.name}.zip")
}

View File

@ -0,0 +1,26 @@
plugins {
id 'org.springframework.boot' version '{bootVersion}'
id 'io.spring.dependency-management' version '{dependencyManagementPluginVersion}'
id 'java'
id 'war'
}
repositories {
mavenCentral()
maven { url 'https://repo.spring.io/milestone' }
maven { url 'https://repo.spring.io/snapshot' }
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web:{bootVersion}")
providedRuntime("org.springframework.boot:spring-boot-starter-tomcat:{bootVersion}")
}
war {
enabled = true
archiveClassifier.set('plain')
}
bootBuildImage {
archiveFile = war.archiveFile
}

View File

@ -0,0 +1,24 @@
plugins {
id 'org.springframework.boot' version '{bootVersion}'
id 'io.spring.dependency-management' version '{dependencyManagementPluginVersion}'
id 'java'
}
repositories {
mavenCentral()
maven { url 'https://repo.spring.io/milestone' }
maven { url 'https://repo.spring.io/snapshot' }
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web:{bootVersion}")
}
bootJar {
manifest {
attributes(
'Implementation-Version': '1.0.0',
'Implementation-Title': "Paketo Test"
)
}
}

View File

@ -0,0 +1,7 @@
pluginManagement {
repositories {
maven { url 'https://repo.spring.io/milestone' }
maven { url 'https://repo.spring.io/snapshot' }
gradlePluginPortal()
}
}

View File

@ -45,7 +45,9 @@
<suppress files="SampleJUnitVintageApplicationTests" checks="SpringJUnit5" />
<suppress files="[\\/]spring-boot-smoke-tests[\\/]" checks="SpringJavadoc" message="\@since" />
<suppress files="[\\/]spring-boot-smoke-tests[\\/]spring-boot-smoke-test-testng[\\/]" checks="SpringJUnit5" />
<suppress files="[\\/]spring-boot-test-support[\\/]" checks="SpringJavadoc" message="\@since" />
<suppress files="[\\/]src[\\/]intTest[\\/]java[\\/]" checks="SpringJavadoc" message="\@since" />
<suppress files="[\\/]src[\\/]systemTest[\\/]java[\\/]" checks="SpringJavadoc" message="\@since" />
<suppress files="LinuxDomainSocket" checks="FinalClass" message="SockaddrUn" />
<suppress files="BsdDomainSocket" checks="FinalClass" message="SockaddrUn" />
<suppress files="StringSequence" checks="SpringMethodVisibility" message="isEmpty"/>