/* * SPDX-License-Identifier: Apache-2.0 * * The OpenSearch Contributors require contributions made to * this file be licensed under the Apache-2.0 license or a * compatible open source license. */ package org.opensearch.gradle.pluginzip; import org.gradle.api.Project; import org.gradle.testfixtures.ProjectBuilder; import org.gradle.testkit.runner.BuildResult; import org.gradle.testkit.runner.GradleRunner; import org.gradle.testkit.runner.UnexpectedBuildFailure; import org.opensearch.gradle.test.GradleUnitTestCase; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.rules.TemporaryFolder; import java.io.File; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.Writer; import java.net.URISyntaxException; import java.net.URL; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import static org.gradle.testkit.runner.TaskOutcome.SUCCESS; import org.apache.maven.model.Model; import org.apache.maven.model.io.xpp3.MavenXpp3Reader; import org.codehaus.plexus.util.xml.pull.XmlPullParserException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; public class PublishTests extends GradleUnitTestCase { private TemporaryFolder projectDir; private static final String TEMPLATE_RESOURCE_FOLDER = "pluginzip"; private final String PROJECT_NAME = "sample-plugin"; private final String ZIP_PUBLISH_TASK = "publishPluginZipPublicationToZipStagingRepository"; @Before public void setUp() throws IOException { projectDir = new TemporaryFolder(); projectDir.create(); } @After public void tearDown() { projectDir.delete(); } /** * This test is used to verify that adding the 'opensearch.pluginzip' to the project * adds some other transitive plugins and tasks under the hood. This is basically * a behavioral test of the {@link Publish#apply(Project)} method. * * This is equivalent of having a build.gradle script with just the following section: *
* plugins {
* id 'opensearch.pluginzip'
* }
*
*/
@Test
public void applyZipPublicationPluginNoConfig() {
// All we do here is creating an empty project and applying the Publish plugin.
Project project = ProjectBuilder.builder().build();
project.getPluginManager().apply(Publish.class);
// WARNING: =====================================================================
// All the following tests will work only before the gradle project is evaluated.
// There are some methods that will cause the project to be evaluated, such as:
// project.getTasksByName()
// After the project is evaluated there are more tasks found in the project, like
// the [assemble, build, ...] and other standard tasks.
// This can potentially break in future gradle versions (?)
// ===============================================================================
assertEquals(
"The Publish plugin is applied which adds total of five tasks from Nebula and MavenPublishing plugins.",
5,
project.getTasks().size()
);
// Tasks applied from "com.netflix.nebula.maven-base-publish"
assertTrue(
project.getTasks()
.findByName("generateMetadataFileForNebulaPublication") instanceof org.gradle.api.publish.tasks.GenerateModuleMetadata
);
assertTrue(
project.getTasks()
.findByName("generatePomFileForNebulaPublication") instanceof org.gradle.api.publish.maven.tasks.GenerateMavenPom
);
assertTrue(
project.getTasks()
.findByName("publishNebulaPublicationToMavenLocal") instanceof org.gradle.api.publish.maven.tasks.PublishToMavenLocal
);
// Tasks applied from MavenPublishPlugin
assertTrue(project.getTasks().findByName("publishToMavenLocal") instanceof org.gradle.api.DefaultTask);
assertTrue(project.getTasks().findByName("publish") instanceof org.gradle.api.DefaultTask);
// And we miss the pluginzip publication task (because no publishing was defined for it)
assertNull(project.getTasks().findByName(ZIP_PUBLISH_TASK));
// We have the following publishing plugins
assertEquals(4, project.getPlugins().size());
// ... of the following types:
assertNotNull(
"Project is expected to have OpenSearch pluginzip Publish plugin",
project.getPlugins().findPlugin(org.opensearch.gradle.pluginzip.Publish.class)
);
assertNotNull(
"Project is expected to have MavenPublishPlugin (applied from OpenSearch pluginzip plugin)",
project.getPlugins().findPlugin(org.gradle.api.publish.maven.plugins.MavenPublishPlugin.class)
);
assertNotNull(
"Project is expected to have Publishing plugin (applied from MavenPublishPublish plugin)",
project.getPlugins().findPlugin(org.gradle.api.publish.plugins.PublishingPlugin.class)
);
assertNotNull(
"Project is expected to have nebula MavenNebulaPublishPlugin plugin (applied from OpenSearch pluginzip plugin)",
project.getPlugins().findPlugin(nebula.plugin.publishing.maven.MavenNebulaPublishPlugin.class)
);
}
/**
* Verify that if the zip publication is configured then relevant tasks are chained correctly.
* This test that the dependsOn() is applied correctly.
*/
@Test
public void applyZipPublicationPluginWithConfig() throws IOException, URISyntaxException, InterruptedException {
/* -------------------------------
// The ideal approach would be to create a project (via ProjectBuilder) with publishzip plugin,
// have it evaluated (API call) and then check if there are tasks that the plugin uses to hookup into
// and how these tasks are chained. The problem is that there is a known gradle issue (#20301) that does
// not allow for it ATM. If, however, it is fixed in the future the following is the code that can
// be used...
Project project = ProjectBuilder.builder().build();
project.getPluginManager().apply(Publish.class);
// add publications via API
// evaluate the project
((DefaultProject)project).evaluate();
// - Check that "validatePluginZipPom" and/or "publishPluginZipPublicationToZipStagingRepository"
// tasks have dependencies on "generatePomFileForNebulaPublication".
// - Check that there is the staging repository added.
// However, due to known issue(1): https://github.com/gradle/gradle/issues/20301
// it is impossible to reach to individual tasks and work with them.
// (1): https://docs.gradle.org/7.4/release-notes.html#known-issues
// I.e.: The following code throws exception, basically any access to individual tasks fails.
project.getTasks().getByName("validatePluginZipPom");
------------------------------- */
// Instead, we run the gradle project via GradleRunner (this way we get fully evaluated project)
// and using the minimal possible configuration (missingPOMEntity) we test that as soon as the zip publication
// configuration is specified then all the necessary tasks are hooked up and executed correctly.
// However, this does not test execution order of the tasks.
GradleRunner runner = prepareGradleRunnerFromTemplate("missingPOMEntity.gradle", ZIP_PUBLISH_TASK/*, "-m"*/);
BuildResult result = runner.build();
assertEquals(SUCCESS, result.task(":" + "bundlePlugin").getOutcome());
assertEquals(SUCCESS, result.task(":" + "generatePomFileForNebulaPublication").getOutcome());
assertEquals(SUCCESS, result.task(":" + "generatePomFileForPluginZipPublication").getOutcome());
assertEquals(SUCCESS, result.task(":" + ZIP_PUBLISH_TASK).getOutcome());
}
/**
* If the plugin is used but relevant publication is not defined then a message is printed.
*/
@Test
public void missingPublications() throws IOException, URISyntaxException {
GradleRunner runner = prepareGradleRunnerFromTemplate("missingPublications.gradle", "build", "-m");
BuildResult result = runner.build();
assertTrue(result.getOutput().contains("Plugin 'opensearch.pluginzip' is applied but no 'pluginZip' publication is defined."));
}
@Test
public void missingGroupValue() throws IOException, URISyntaxException, XmlPullParserException {
GradleRunner runner = prepareGradleRunnerFromTemplate("missingGroupValue.gradle", "build", ZIP_PUBLISH_TASK);
Exception e = assertThrows(UnexpectedBuildFailure.class, runner::build);
assertTrue(e.getMessage().contains("Invalid publication 'pluginZip': groupId cannot be empty."));
}
/**
* This would be the most common use case where user declares Maven publication entity with minimal info
* and the resulting POM file will use artifactId, groupId and version values based on the Gradle project object.
*/
@Test
public void useDefaultValues() throws IOException, URISyntaxException, XmlPullParserException {
GradleRunner runner = prepareGradleRunnerFromTemplate("useDefaultValues.gradle", "build", ZIP_PUBLISH_TASK);
BuildResult result = runner.build();
/** Check if build and {@value ZIP_PUBLISH_TASK} tasks have run well */
assertEquals(SUCCESS, result.task(":" + "build").getOutcome());
assertEquals(SUCCESS, result.task(":" + ZIP_PUBLISH_TASK).getOutcome());
// check if both the zip and pom files have been published to local staging repo
assertTrue(
new File(
projectDir.getRoot(),
String.join(
File.separator,
"build",
"local-staging-repo",
"org",
"custom",
"group",
PROJECT_NAME,
"2.0.0.0",
PROJECT_NAME + "-2.0.0.0.pom"
)
).exists()
);
assertTrue(
new File(
projectDir.getRoot(),
String.join(
File.separator,
"build",
"local-staging-repo",
"org",
"custom",
"group",
PROJECT_NAME,
"2.0.0.0",
PROJECT_NAME + "-2.0.0.0.zip"
)
).exists()
);
// Parse the maven file and validate default values
MavenXpp3Reader reader = new MavenXpp3Reader();
Model model = reader.read(
new FileReader(
new File(
projectDir.getRoot(),
String.join(
File.separator,
"build",
"local-staging-repo",
"org",
"custom",
"group",
PROJECT_NAME,
"2.0.0.0",
PROJECT_NAME + "-2.0.0.0.pom"
)
)
)
);
assertEquals(model.getVersion(), "2.0.0.0");
assertEquals(model.getGroupId(), "org.custom.group");
assertEquals(model.getArtifactId(), PROJECT_NAME);
assertNull(model.getName());
assertNull(model.getDescription());
assertEquals(model.getUrl(), "https://github.com/doe/sample-plugin");
}
/**
* If the `group` is defined in gradle's allprojects section then it does not have to defined in publications.
*/
@Test
public void allProjectsGroup() throws IOException, URISyntaxException, XmlPullParserException {
GradleRunner runner = prepareGradleRunnerFromTemplate("allProjectsGroup.gradle", "build", ZIP_PUBLISH_TASK);
BuildResult result = runner.build();
/** Check if build and {@value ZIP_PUBLISH_TASK} tasks have run well */
assertEquals(SUCCESS, result.task(":" + "build").getOutcome());
assertEquals(SUCCESS, result.task(":" + ZIP_PUBLISH_TASK).getOutcome());
// Parse the maven file and validate default values
MavenXpp3Reader reader = new MavenXpp3Reader();
Model model = reader.read(
new FileReader(
new File(
projectDir.getRoot(),
String.join(
File.separator,
"build",
"local-staging-repo",
"org",
"opensearch",
PROJECT_NAME,
"2.0.0.0",
PROJECT_NAME + "-2.0.0.0.pom"
)
)
)
);
assertEquals(model.getVersion(), "2.0.0.0");
assertEquals(model.getGroupId(), "org.opensearch");
}
/**
* The groupId value can be defined on several levels. This tests that the most internal level outweighs other levels.
*/
@Test
public void groupPriorityLevel() throws IOException, URISyntaxException, XmlPullParserException {
GradleRunner runner = prepareGradleRunnerFromTemplate("groupPriorityLevel.gradle", "build", ZIP_PUBLISH_TASK);
BuildResult result = runner.build();
/** Check if build and {@value ZIP_PUBLISH_TASK} tasks have run well */
assertEquals(SUCCESS, result.task(":" + "build").getOutcome());
assertEquals(SUCCESS, result.task(":" + ZIP_PUBLISH_TASK).getOutcome());
// Parse the maven file and validate default values
MavenXpp3Reader reader = new MavenXpp3Reader();
Model model = reader.read(
new FileReader(
new File(
projectDir.getRoot(),
String.join(
File.separator,
"build",
"local-staging-repo",
"level",
"3",
PROJECT_NAME,
"2.0.0.0",
PROJECT_NAME + "-2.0.0.0.pom"
)
)
)
);
assertEquals(model.getVersion(), "2.0.0.0");
assertEquals(model.getGroupId(), "level.3");
}
/**
* In this case the Publication entity is completely missing but still the POM file is generated using the default
* values including the groupId and version values obtained from the Gradle project object.
*/
@Test
public void missingPOMEntity() throws IOException, URISyntaxException, XmlPullParserException {
GradleRunner runner = prepareGradleRunnerFromTemplate("missingPOMEntity.gradle", "build", ZIP_PUBLISH_TASK);
BuildResult result = runner.build();
/** Check if build and {@value ZIP_PUBLISH_TASK} tasks have run well */
assertEquals(SUCCESS, result.task(":" + "build").getOutcome());
assertEquals(SUCCESS, result.task(":" + ZIP_PUBLISH_TASK).getOutcome());
// Parse the maven file and validate it
MavenXpp3Reader reader = new MavenXpp3Reader();
Model model = reader.read(
new FileReader(
new File(
projectDir.getRoot(),
String.join(
File.separator,
"build",
"local-staging-repo",
"org",
"custom",
"group",
PROJECT_NAME,
"2.0.0.0",
PROJECT_NAME + "-2.0.0.0.pom"
)
)
)
);
assertEquals(model.getArtifactId(), PROJECT_NAME);
assertEquals(model.getGroupId(), "org.custom.group");
assertEquals(model.getVersion(), "2.0.0.0");
assertEquals(model.getPackaging(), "zip");
assertNull(model.getName());
assertNull(model.getDescription());
assertEquals(0, model.getDevelopers().size());
assertEquals(0, model.getContributors().size());
assertEquals(0, model.getLicenses().size());
}
/**
* In some cases we need the POM groupId value to be different from the Gradle "project.group" value hence we
* allow for groupId customization (it will override whatever the Gradle "project.group" value is).
*/
@Test
public void customizedGroupValue() throws IOException, URISyntaxException, XmlPullParserException {
GradleRunner runner = prepareGradleRunnerFromTemplate("customizedGroupValue.gradle", "build", ZIP_PUBLISH_TASK);
BuildResult result = runner.build();
/** Check if build and {@value ZIP_PUBLISH_TASK} tasks have run well */
assertEquals(SUCCESS, result.task(":" + "build").getOutcome());
assertEquals(SUCCESS, result.task(":" + ZIP_PUBLISH_TASK).getOutcome());
// Parse the maven file and validate the groupID
MavenXpp3Reader reader = new MavenXpp3Reader();
Model model = reader.read(
new FileReader(
new File(
projectDir.getRoot(),
String.join(
File.separator,
"build",
"local-staging-repo",
"I",
"am",
"customized",
PROJECT_NAME,
"2.0.0.0",
PROJECT_NAME + "-2.0.0.0.pom"
)
)
)
);
assertEquals(model.getGroupId(), "I.am.customized");
}
/**
* If the customized groupId value is invalid (from the Maven POM perspective) then we need to be sure it is
* caught and reported properly.
*/
@Test
public void customizedInvalidGroupValue() throws IOException, URISyntaxException {
GradleRunner runner = prepareGradleRunnerFromTemplate("customizedInvalidGroupValue.gradle", "build", ZIP_PUBLISH_TASK);
Exception e = assertThrows(UnexpectedBuildFailure.class, runner::build);
assertTrue(
e.getMessage().contains("Invalid publication 'pluginZip': groupId ( ) is not a valid Maven identifier ([A-Za-z0-9_\\-.]+).")
);
}
/**
* This test verifies that use of the pluginZip does not clash with other maven publication plugins.
* It covers the case when user calls the "publishToMavenLocal" task.
*/
@Test
public void publishToMavenLocal() throws IOException, URISyntaxException, XmlPullParserException {
// By default, the "publishToMavenLocal" publishes artifacts to a local m2 repo, typically
// found in `~/.m2/repository`. But this is not practical for this unit test at all. We need to point
// the 'maven-publish' plugin to a custom m2 repo located in temporary directory associated with this
// test case instead.
//
// According to Gradle documentation this should be possible by proper configuration of the publishing
// task (https://docs.gradle.org/current/userguide/publishing_maven.html#publishing_maven:install).
// But for some reason this never worked as expected and artifacts created during this test case
// were always pushed into the default local m2 repository (ie: `~/.m2/repository`).
// The only workaround that seems to work is to pass "-Dmaven.repo.local" property via runner argument.
// (Kudos to: https://stackoverflow.com/questions/72265294/gradle-publishtomavenlocal-specify-custom-directory)
//
// The temporary directory that is used as the local m2 repository is created via in task "prepareLocalMVNRepo".
GradleRunner runner = prepareGradleRunnerFromTemplate(
"publishToMavenLocal.gradle",
String.join(File.separator, "-Dmaven.repo.local=" + projectDir.getRoot(), "build", "local-staging-repo"),
"build",
"prepareLocalMVNRepo",
"publishToMavenLocal"
);
BuildResult result = runner.build();
assertEquals(SUCCESS, result.task(":" + "build").getOutcome());
assertEquals(SUCCESS, result.task(":" + "publishToMavenLocal").getOutcome());
// Parse the maven file and validate it
MavenXpp3Reader reader = new MavenXpp3Reader();
Model model = reader.read(
new FileReader(
new File(
projectDir.getRoot(),
String.join(
File.separator,
"build",
"local-staging-repo",
"org",
"custom",
"group",
PROJECT_NAME,
"2.0.0.0",
PROJECT_NAME + "-2.0.0.0.pom"
)
)
)
);
// The "publishToMavenLocal" task will run ALL maven publications, hence we can expect the ZIP publication
// present as well: https://docs.gradle.org/current/userguide/publishing_maven.html#publishing_maven:tasks
assertEquals(model.getArtifactId(), PROJECT_NAME);
assertEquals(model.getGroupId(), "org.custom.group");
assertEquals(model.getVersion(), "2.0.0.0");
assertEquals(model.getPackaging(), "zip");
// We have two publications in the build.gradle file, both are "MavenPublication" based.
// Both the mavenJava and pluginZip publications publish to the same location (coordinates) and
// artifacts (the POM file) overwrite each other. However, we can verify that the Zip plugin is
// the last one and "wins" over the mavenJava.
assertEquals(model.getDescription(), "pluginZip publication");
}
/**
* A helper method for use cases
*
* @param templateName The name of the file (from "pluginzip" folder) to use as a build.gradle for the test
* @param gradleArguments Optional CLI parameters to pass into Gradle runner
*/
private GradleRunner prepareGradleRunnerFromTemplate(String templateName, String... gradleArguments) throws IOException,
URISyntaxException {
useTemplateFile(projectDir.newFile("build.gradle"), templateName);
prepareGradleFilesAndSources();
GradleRunner runner = GradleRunner.create()
.forwardOutput()
.withPluginClasspath()
.withArguments(gradleArguments)
.withProjectDir(projectDir.getRoot());
return runner;
}
private void prepareGradleFilesAndSources() throws IOException {
// A dummy "source" file that is processed with bundlePlugin and put into a ZIP artifact file
File bundleFile = new File(projectDir.getRoot(), PROJECT_NAME + "-source.txt");
Files.createFile(bundleFile.toPath());
// Setting a project name via settings.gradle file
writeString(projectDir.newFile("settings.gradle"), "rootProject.name = '" + PROJECT_NAME + "'");
}
private void writeString(File file, String string) throws IOException {
try (Writer writer = new FileWriter(file)) {
writer.write(string);
}
}
/**
* Write the content of the "template" file into the target file.
* The template file must be located in the {@value TEMPLATE_RESOURCE_FOLDER} folder.
* @param targetFile A target file
* @param templateFile A name of the template file located under {@value TEMPLATE_RESOURCE_FOLDER} folder
*/
private void useTemplateFile(File targetFile, String templateFile) throws IOException, URISyntaxException {
URL resource = getClass().getClassLoader().getResource(String.join(File.separator, TEMPLATE_RESOURCE_FOLDER, templateFile));
Path resPath = Paths.get(resource.toURI()).toAbsolutePath();
List