diff --git a/src/main/java/org/jenkinsci/test/acceptance/plugins/dashboard_view/DashboardView.java b/src/main/java/org/jenkinsci/test/acceptance/plugins/dashboard_view/DashboardView.java index 5730862c7c..1fa16a8eb6 100644 --- a/src/main/java/org/jenkinsci/test/acceptance/plugins/dashboard_view/DashboardView.java +++ b/src/main/java/org/jenkinsci/test/acceptance/plugins/dashboard_view/DashboardView.java @@ -1,19 +1,14 @@ package org.jenkinsci.test.acceptance.plugins.dashboard_view; -import com.google.inject.Injector; -import org.hamcrest.Description; -import org.jenkinsci.test.acceptance.Matcher; -import org.jenkinsci.test.acceptance.plugins.analysis_collector.AnalysisPlugin; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; + import org.jenkinsci.test.acceptance.po.Control; import org.jenkinsci.test.acceptance.po.Describable; -import org.jenkinsci.test.acceptance.po.Job; import org.jenkinsci.test.acceptance.po.View; -import org.openqa.selenium.NoSuchElementException; -import org.openqa.selenium.WebElement; -import java.net.URL; -import java.util.ArrayList; -import java.util.List; +import com.google.inject.Injector; /** * @author Kohsuke Kawaguchi @@ -57,25 +52,4 @@ public T getBottomPortlet(Class port } throw new java.util.NoSuchElementException(); } - - public static Matcher hasWarningsFor(final Job job, final AnalysisPlugin plugin, final int warningsCount) { - return new Matcher(" shows %s warnings for plugin %s and job %s", warningsCount, plugin.getId(), job.name) { - @Override - public boolean matchesSafely(final DashboardView view) { - view.open(); - try { - WebElement warningsLink = view.find(by.css("a[href='job/" + job.name + "/" + plugin.getId() + "']")); - String linkText = warningsLink.getText(); - return Integer.parseInt(linkText) == warningsCount; - } catch (NoSuchElementException | NumberFormatException e) { - return false; - } - } - - @Override - public void describeMismatchSafely(final DashboardView view, final Description desc) { - desc.appendText("Portlet does not show expected warnings for plugin " + plugin.getId()); - } - }; - } } diff --git a/src/main/java/org/jenkinsci/test/acceptance/plugins/git/GitRepo.java b/src/main/java/org/jenkinsci/test/acceptance/plugins/git/GitRepo.java index 67faac4393..96390dcd2a 100644 --- a/src/main/java/org/jenkinsci/test/acceptance/plugins/git/GitRepo.java +++ b/src/main/java/org/jenkinsci/test/acceptance/plugins/git/GitRepo.java @@ -1,18 +1,35 @@ package org.jenkinsci.test.acceptance.plugins.git; -import com.jcraft.jsch.*; +import java.io.BufferedReader; +import java.io.Closeable; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Properties; + import org.apache.commons.io.FileUtils; import org.jenkinsci.test.acceptance.docker.fixtures.GitContainer; import org.zeroturnaround.zip.ZipUtil; -import java.io.*; -import java.nio.file.Files; -import java.util.*; +import com.jcraft.jsch.ChannelExec; +import com.jcraft.jsch.ChannelSftp; +import com.jcraft.jsch.JSch; +import com.jcraft.jsch.JSchException; +import com.jcraft.jsch.Session; +import com.jcraft.jsch.SftpException; -import static java.lang.ProcessBuilder.Redirect.INHERIT; -import static java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE; -import static java.nio.file.attribute.PosixFilePermission.OWNER_READ; -import static java.util.Collections.singleton; +import static java.lang.ProcessBuilder.Redirect.*; +import static java.nio.file.attribute.PosixFilePermission.*; +import static java.util.Collections.*; import static org.jenkinsci.test.acceptance.docker.fixtures.GitContainer.*; /** @@ -107,13 +124,13 @@ public void commit(String msg) throws IOException, InterruptedException { } public void touch(String name) throws IOException { - FileUtils.writeStringToFile(path(name), ""); + FileUtils.writeStringToFile(file(name), ""); } /** - * Refers to a path relative to the workspace directory. + * Refers to a file relative to the workspace directory. */ - public File path(String name) { + public File file(String name) { return new File(dir, name); } @@ -163,10 +180,10 @@ public File createTempDir(String name) throws IOException { * @param port SSH port of Docker container */ public void transferToDockerContainer(String host, int port) throws IOException, InterruptedException, JSchException, SftpException { - - String zippedFilename = "repo.zip"; - File zipppedRepo = new File(dir.getPath() + "/" + zippedFilename); - ZipUtil.pack(new File(dir.getPath()), zipppedRepo); + Path zipPath = Files.createTempFile("git", "zip"); + File zippedRepo = zipPath.toFile(); + String zippedFilename = zipPath.getFileName().toString(); + ZipUtil.pack(new File(dir.getPath()), zippedRepo); Properties props = new Properties(); props.put("StrictHostKeyChecking", "no"); @@ -181,7 +198,7 @@ public void transferToDockerContainer(String host, int port) throws IOException, ChannelSftp channel = (ChannelSftp) session.openChannel("sftp"); channel.connect(); channel.cd("/home/git"); - channel.put(new FileInputStream(zipppedRepo), zippedFilename); + channel.put(new FileInputStream(zippedRepo), zippedFilename); ChannelExec channelExec = (ChannelExec) session.openChannel("exec"); InputStream in = channelExec.getInputStream(); @@ -198,5 +215,10 @@ public void transferToDockerContainer(String host, int port) throws IOException, channelExec.disconnect(); channel.disconnect(); session.disconnect(); + Files.delete(zipPath); + } + + public Path path(Path path) { + return dir.toPath().resolve(path); } } diff --git a/src/main/java/org/jenkinsci/test/acceptance/plugins/nested_view/NestedView.java b/src/main/java/org/jenkinsci/test/acceptance/plugins/nested_view/NestedView.java index a9d84220e8..c655f65d88 100644 --- a/src/main/java/org/jenkinsci/test/acceptance/plugins/nested_view/NestedView.java +++ b/src/main/java/org/jenkinsci/test/acceptance/plugins/nested_view/NestedView.java @@ -1,11 +1,12 @@ package org.jenkinsci.test.acceptance.plugins.nested_view; -import com.google.inject.Injector; +import java.net.URL; import org.jenkinsci.test.acceptance.po.Describable; import org.jenkinsci.test.acceptance.po.View; import org.jenkinsci.test.acceptance.po.ViewsMixIn; -import java.net.URL; + +import com.google.inject.Injector; /** * @author Kohsuke Kawaguchi @@ -31,4 +32,8 @@ public void assertActiveView(String name) { public void assertInactiveView(String name) { find(by.xpath("//*[contains(@class, 'inactive') or not(contains(@class, 'active'))]/a[text()='%s']", name)); } + + public ViewsMixIn getViews() { + return views; + } } diff --git a/src/main/java/org/jenkinsci/test/acceptance/plugins/workflow_multibranch/GitBranchSource.java b/src/main/java/org/jenkinsci/test/acceptance/plugins/workflow_multibranch/GitBranchSource.java new file mode 100644 index 0000000000..2de4365ca5 --- /dev/null +++ b/src/main/java/org/jenkinsci/test/acceptance/plugins/workflow_multibranch/GitBranchSource.java @@ -0,0 +1,35 @@ +package org.jenkinsci.test.acceptance.plugins.workflow_multibranch; + +import org.jenkinsci.test.acceptance.po.Control; +import org.jenkinsci.test.acceptance.po.Describable; +import org.jenkinsci.test.acceptance.po.WorkflowMultiBranchJob; +import org.openqa.selenium.By; +import org.openqa.selenium.support.ui.Select; + +/** + * Git Branch Source for the pipeline multi-branch plugin. + * + * @author Ullrich Hafner + */ +@Describable("Git") +// TODO: Remove duplicates with GitScm +public class GitBranchSource extends BranchSource { + private final Control remote = control("remote"); + + public GitBranchSource(WorkflowMultiBranchJob job, String path) { + super(job, path); + } + + public GitBranchSource setRemote(final String remoteUrl) { + this.remote.set(remoteUrl); + + return this; + } + + public GitBranchSource setCredentials(final String name) { + Select select = new Select(control(By.className("credentials-select")).resolve()); + select.selectByVisibleText(name); + + return this; + } +} \ No newline at end of file diff --git a/src/main/java/org/jenkinsci/test/acceptance/po/View.java b/src/main/java/org/jenkinsci/test/acceptance/po/View.java index 29b9ced121..230210d280 100644 --- a/src/main/java/org/jenkinsci/test/acceptance/po/View.java +++ b/src/main/java/org/jenkinsci/test/acceptance/po/View.java @@ -1,16 +1,16 @@ package org.jenkinsci.test.acceptance.po; -import com.fasterxml.jackson.databind.JsonNode; -import com.google.inject.Injector; - import java.net.URL; import org.hamcrest.Description; import org.jenkinsci.test.acceptance.Matcher; -import static org.hamcrest.CoreMatchers.not; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.jenkinsci.test.acceptance.Matchers.hasContent; +import com.fasterxml.jackson.databind.JsonNode; +import com.google.inject.Injector; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.*; +import static org.jenkinsci.test.acceptance.Matchers.*; /** * Page object for view, which is a collection of jobs rendered in the UI. @@ -20,6 +20,8 @@ * @author Kohsuke Kawaguchi */ public abstract class View extends ContainerPageObject { + private final Control recurseIntoFolder = control("/recurse"); + public final JobsMixIn jobs; public View(Injector injector, URL url) { @@ -85,4 +87,8 @@ public void describeMismatchSafely(View view, Description mismatchDescription) { } }; } + + public void checkRecurseIntoFolders() { + recurseIntoFolder.check(); + } } diff --git a/src/test/java/plugins/AnalysisCollectorPluginTest.java b/src/test/java/plugins/AnalysisCollectorPluginTest.java index 170e47aae1..7de46a26f1 100644 --- a/src/test/java/plugins/AnalysisCollectorPluginTest.java +++ b/src/test/java/plugins/AnalysisCollectorPluginTest.java @@ -1,9 +1,22 @@ package plugins; +import javax.inject.Inject; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.List; import java.util.Map; +import org.hamcrest.Description; +import org.jenkinsci.test.acceptance.Matcher; +import org.jenkinsci.test.acceptance.docker.DockerContainerHolder; +import org.jenkinsci.test.acceptance.docker.fixtures.GitContainer; import org.jenkinsci.test.acceptance.junit.Since; +import org.jenkinsci.test.acceptance.junit.WithCredentials; +import org.jenkinsci.test.acceptance.junit.WithDocker; import org.jenkinsci.test.acceptance.junit.WithPlugins; import org.jenkinsci.test.acceptance.plugins.analysis_collector.AnalysisCollectorAction; import org.jenkinsci.test.acceptance.plugins.analysis_collector.AnalysisCollectorColumn; @@ -16,22 +29,34 @@ import org.jenkinsci.test.acceptance.plugins.checkstyle.CheckStyleFreestyleSettings; import org.jenkinsci.test.acceptance.plugins.dashboard_view.DashboardView; import org.jenkinsci.test.acceptance.plugins.findbugs.FindBugsFreestyleSettings; +import org.jenkinsci.test.acceptance.plugins.git.GitRepo; +import org.jenkinsci.test.acceptance.plugins.nested_view.NestedView; import org.jenkinsci.test.acceptance.plugins.pmd.PmdFreestyleSettings; import org.jenkinsci.test.acceptance.plugins.tasks.TasksFreestyleSettings; import org.jenkinsci.test.acceptance.plugins.warnings.WarningsBuildSettings; +import org.jenkinsci.test.acceptance.plugins.workflow_multibranch.GitBranchSource; import org.jenkinsci.test.acceptance.po.Build; import org.jenkinsci.test.acceptance.po.Container; +import org.jenkinsci.test.acceptance.po.Folder; import org.jenkinsci.test.acceptance.po.FreeStyleJob; import org.jenkinsci.test.acceptance.po.Job; import org.jenkinsci.test.acceptance.po.ListView; +import org.jenkinsci.test.acceptance.po.TopLevelItem; import org.jenkinsci.test.acceptance.po.WorkflowJob; +import org.jenkinsci.test.acceptance.po.WorkflowMultiBranchJob; import org.junit.Test; import org.jvnet.hudson.test.Issue; import org.openqa.selenium.By; +import org.openqa.selenium.NoSuchElementException; import org.openqa.selenium.WebElement; +import com.jcraft.jsch.JSchException; +import com.jcraft.jsch.SftpException; + import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.collection.IsCollectionWithSize.*; import static org.jenkinsci.test.acceptance.Matchers.*; import static org.jenkinsci.test.acceptance.plugins.analysis_collector.AnalysisPlugin.*; import static org.jenkinsci.test.acceptance.plugins.dashboard_view.DashboardView.*; @@ -69,6 +94,9 @@ public class AnalysisCollectorPluginTest extends AbstractAnalysisTest gitForMultiBranch; + /** * Builds a freestyle job. Verifies that afterwards a trend graph exists for each of the participating plug-ins. * Finally, the collector trend graph is verified that contains 6 relative links to the @@ -273,6 +301,88 @@ public void should_set_warnings_count_in_list_view_column() { assertThat(tooltip, not(containsString("" + PMD_ALL + ""))); } + /** + * Sets up a nested view that contains a dashboard view with a warnings-per-project portlet. + * Creates a folder in this view and a multi-branch job in this folder. + * The multi-branch job is based on a git repository with two branches (master and branch). + * Each branch contains a Jenkinsfile and several warnings results files. + * Builds the jobs and verifies that the portlet is correctly filled and that all links open the correct page. + */ + @Test @Issue("JENKINS-39950") @WithPlugins({"dashboard-view", "nested-view"}) + @WithDocker @WithCredentials(credentialType = WithCredentials.SSH_USERNAME_PRIVATE_KEY, values = {"warnings", "/org/jenkinsci/test/acceptance/docker/fixtures/GitContainer/unsafe"}) + public void should_show_correct_links() throws InterruptedException, SftpException, JSchException, IOException { + NestedView nested = jenkins.getViews().create(NestedView.class, "Nested"); + + DashboardView dashboard = nested.getViews().create(DashboardView.class, "Dashboard"); + dashboard.configure(); + dashboard.matchAllJobs(); + dashboard.checkRecurseIntoFolders(); + + addWarningsPortlet(dashboard); + dashboard.save(); + + Folder folder = dashboard.jobs.create(Folder.class, "Folder"); + folder.save(); + folder.open(); + + String repoUrl = createGitRepositoryInDockerContainer(); + + WorkflowMultiBranchJob job = folder.getJobs().create(WorkflowMultiBranchJob.class); + GitBranchSource branchSource = job.addBranchSource(GitBranchSource.class); + branchSource.setRemote(repoUrl); + branchSource.setCredentials("warnings"); + + job.save(); + job.waitForBranchIndexingFinished(20); + + WorkflowJob master = job.getJob("master"); + master.build(1).waitUntilFinished().shouldSucceed(); + WorkflowJob branch = job.getJob("branch"); + branch.build(1).waitUntilFinished().shouldSucceed(); + + dashboard.open(); + + verifyWarningsCountInPortlet(master, dashboard); + verifyWarningsCountInPortlet(branch, dashboard); + + // FIXME: check that the links are broken + List links = all(By.linkText(String.valueOf(CHECKSTYLE_ALL))); + assertThat(links, hasSize(2)); + + } + + private String createGitRepositoryInDockerContainer() { + try { + GitRepo repo = new GitRepo(); + Path source = Paths.get(getClass().getResource(ANALYSIS_COLLECTOR_PLUGIN_RESOURCES).toURI()); + + try (DirectoryStream paths = Files.newDirectoryStream(source)) { + for (Path path : paths) { + Files.copy(path, repo.path(path.getFileName())); + } + } + repo.git("add", "*"); + repo.git("commit", "-m", "Initial commit in master"); + repo.git("branch", "branch"); + + GitContainer container = gitForMultiBranch.get(); + repo.transferToDockerContainer(container.host(), container.port()); + + return container.getRepoUrl(); + } + catch (IOException | InterruptedException | URISyntaxException | JSchException | SftpException e) { + throw new AssertionError(e); + } + } + + private WarningsPerProjectPortlet addWarningsPortlet(DashboardView dashboard) { + WarningsPerProjectPortlet portlet = dashboard.addBottomPortlet(WarningsPerProjectPortlet.class); + portlet.setName("My Warnings"); + portlet.hideZeroWarningsProjects(false); + portlet.showImagesInTableHeader(true); + return portlet; + } + /** * Sets up a dashboard view with a warnings-per-project portlet. Builds a job and checks if the portlet shows the * correct number of warnings. Then one of the tools is deselected. The portlet should then show only the remaining @@ -286,26 +396,25 @@ public void should_aggregate_warnings_in_dashboard_portlet() { DashboardView dashboard = jenkins.views.create(DashboardView.class, createRandomName()); dashboard.configure(); dashboard.matchAllJobs(); - WarningsPerProjectPortlet portlet = dashboard.addBottomPortlet(WarningsPerProjectPortlet.class); - portlet.setName("My Warnings"); - portlet.hideZeroWarningsProjects(false); - portlet.showImagesInTableHeader(true); + WarningsPerProjectPortlet portlet = addWarningsPortlet(dashboard); dashboard.save(); dashboard.open(); + verifyWarningsCountInPortlet(job, dashboard); + + // uncheck Open Tasks + dashboard.configure(() -> portlet.checkCollectedPlugin(TASKS, false)); + dashboard.open(); + + assertThat(dashboard, not(hasWarningsFor(job, TASKS, TASKS_ALL))); + } + + private void verifyWarningsCountInPortlet(TopLevelItem job, DashboardView dashboard) { assertThat(dashboard, hasWarningsFor(job, CHECKSTYLE, CHECKSTYLE_ALL)); assertThat(dashboard, hasWarningsFor(job, PMD, PMD_ALL)); assertThat(dashboard, hasWarningsFor(job, FINDBUGS, FINDBUGS_ALL)); assertThat(dashboard, hasWarningsFor(job, TASKS, TASKS_ALL)); assertThat(dashboard, hasWarningsFor(job, WARNINGS, WARNINGS_ALL)); - - // uncheck Open Tasks - dashboard.configure(); - portlet = dashboard.getBottomPortlet(WarningsPerProjectPortlet.class); - portlet.checkCollectedPlugin(TASKS, false); - dashboard.save(); - dashboard.open(); - assertThat(dashboard, not(hasWarningsFor(job, TASKS, TASKS_ALL))); } @Test @WithPlugins("workflow-aggregator") @@ -454,4 +563,25 @@ private void addAndConfigureTasksPublisher(final FreeStyleJob job) { }; configurator.accept(taskScannerSettings); } + + public static Matcher hasWarningsFor(final TopLevelItem job, final AnalysisPlugin plugin, final int warningsCount) { + return new Matcher(" shows %s warnings for plugin %s and job %s", warningsCount, plugin.getId(), job.name) { + @Override + public boolean matchesSafely(final DashboardView view) { + view.open(); + try { + WebElement warningsLink = view.find(by.css("a[href='job/" + job.name + "/" + plugin.getId() + "']")); + String linkText = warningsLink.getText(); + return Integer.parseInt(linkText) == warningsCount; + } catch (NoSuchElementException | NumberFormatException e) { + return false; + } + } + + @Override + public void describeMismatchSafely(final DashboardView view, final Description desc) { + desc.appendText("Portlet does not show expected warnings for plugin " + plugin.getId()); + } + }; + } } diff --git a/src/test/resources/analysis_collector_plugin/Jenkinsfile b/src/test/resources/analysis_collector_plugin/Jenkinsfile new file mode 100644 index 0000000000..9023bcc855 --- /dev/null +++ b/src/test/resources/analysis_collector_plugin/Jenkinsfile @@ -0,0 +1,15 @@ +node { + checkout scm + + step([$class: 'FindBugsPublisher', pattern: '**/findbugs.xml']) + step([$class: 'PmdPublisher']) + step([$class: 'CheckStylePublisher']) + step([$class: 'TasksPublisher', high: 'PRIO1', normal: 'PRIO2, TODO', low :'PRIO3']) + step([$class: 'WarningsPublisher', + parserConfigurations: [ + [parserName: 'Java Compiler (javac)', pattern: '**/warnings.txt'], + [parserName: 'JavaDoc Tool', pattern: '**/warnings.txt'], + [parserName: 'MSBuild', pattern: '**/warnings.txt'] + ]]) + step([$class: 'AnalysisPublisher']) +} \ No newline at end of file