From e6a58e8e2d30f7faa6941694ff58770cc14f50e7 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Mon, 10 Oct 2016 17:57:52 -0400 Subject: [PATCH] Reimplemented HTML display of Pipeline console to include some features from #21. --- pom.xml | 2 +- .../plugins/workflow/job/WorkflowRun.java | 9 +- .../job/console/NewNodeConsoleNote.java | 104 +++++++++++++++ .../job/console/PipelineLargeText.java | 113 +++++++++++++++-- .../job/console/WorkflowRunConsoleNote.java | 29 ++--- .../workflow/job/WorkflowRun/sidepanel.jelly | 12 +- .../job/console/NewNodeConsoleNote/script.js | 29 +++++ .../job/console/NewNodeConsoleNote/style.css | 3 + .../plugins/workflow/job/WorkflowRunTest.java | 99 --------------- .../job/console/PipelineLargeTextTest.java | 119 ++++++++++++++++++ 10 files changed, 383 insertions(+), 136 deletions(-) create mode 100644 src/main/java/org/jenkinsci/plugins/workflow/job/console/NewNodeConsoleNote.java create mode 100644 src/main/resources/org/jenkinsci/plugins/workflow/job/console/NewNodeConsoleNote/script.js create mode 100644 src/main/resources/org/jenkinsci/plugins/workflow/job/console/NewNodeConsoleNote/style.css create mode 100644 src/test/java/org/jenkinsci/plugins/workflow/job/console/PipelineLargeTextTest.java diff --git a/pom.xml b/pom.xml index baa075d5..20a9836b 100644 --- a/pom.xml +++ b/pom.xml @@ -63,7 +63,7 @@ 1.642.3 - 2.16 + 2.17 diff --git a/src/main/java/org/jenkinsci/plugins/workflow/job/WorkflowRun.java b/src/main/java/org/jenkinsci/plugins/workflow/job/WorkflowRun.java index aff2c579..a3302c28 100644 --- a/src/main/java/org/jenkinsci/plugins/workflow/job/WorkflowRun.java +++ b/src/main/java/org/jenkinsci/plugins/workflow/job/WorkflowRun.java @@ -33,7 +33,6 @@ import hudson.EnvVars; import hudson.Extension; import hudson.FilePath; -import hudson.Main; import hudson.XmlFile; import hudson.console.AnnotatedLargeText; import hudson.console.ConsoleNote; @@ -96,9 +95,9 @@ import org.jenkinsci.plugins.workflow.flow.StashManager; import org.jenkinsci.plugins.workflow.graph.FlowEndNode; import org.jenkinsci.plugins.workflow.graph.FlowNode; +import org.jenkinsci.plugins.workflow.job.console.NewNodeConsoleNote; import org.jenkinsci.plugins.workflow.job.console.PipelineLargeText; import org.jenkinsci.plugins.workflow.job.console.PipelineLogFile; -import org.jenkinsci.plugins.workflow.job.console.WorkflowRunConsoleNote; import org.jenkinsci.plugins.workflow.steps.FlowInterruptedException; import org.jenkinsci.plugins.workflow.steps.StepContext; import org.jenkinsci.plugins.workflow.steps.StepExecution; @@ -704,7 +703,7 @@ private final class GraphL implements GraphListener { */ private final class NodePrintListener implements GraphListener.Synchronous { @Override public void onNewHead(FlowNode node) { - WorkflowRunConsoleNote.print(node.getDisplayFunctionName(), listener); + NewNodeConsoleNote.print(node, listener); } } @@ -756,9 +755,7 @@ private final class NodePrintListener implements GraphListener.Synchronous { } @Override public File getLogFile() { - if (!Main.isUnitTest) { // TODO at least until https://github.com/jenkinsci/jenkins-test-harness/pull/38 - LOGGER.log(Level.WARNING, "Avoid calling getLogFile on " + this, new UnsupportedOperationException()); - } + LOGGER.log(Level.WARNING, "Avoid calling getLogFile on " + this, new UnsupportedOperationException()); try { File f = File.createTempFile("deprecated", ".log", getRootDir()); f.deleteOnExit(); diff --git a/src/main/java/org/jenkinsci/plugins/workflow/job/console/NewNodeConsoleNote.java b/src/main/java/org/jenkinsci/plugins/workflow/job/console/NewNodeConsoleNote.java new file mode 100644 index 00000000..cdd16c20 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/workflow/job/console/NewNodeConsoleNote.java @@ -0,0 +1,104 @@ +/* + * The MIT License + * + * Copyright (c) 2015, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.jenkinsci.plugins.workflow.job.console; + +import hudson.Extension; +import hudson.MarkupText; +import hudson.console.ConsoleAnnotationDescriptor; +import hudson.console.ConsoleAnnotator; +import hudson.console.ConsoleNote; +import hudson.model.Run; +import hudson.model.TaskListener; +import java.io.IOException; +import java.util.List; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import org.jenkinsci.plugins.workflow.graph.BlockEndNode; +import org.jenkinsci.plugins.workflow.graph.FlowNode; +import org.jenkinsci.plugins.workflow.support.actions.AnnotatedLogAction; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +/** + * Console line with note printed when a new {@link FlowNode} is added to the graph. + * Defines the {@code pipeline-new-node} CSS class and several attributes which may be used to control subsequent behavior: + *
    + *
  • {@code nodeId} for {@link FlowNode#getId} + *
  • {@code parentIds} for {@link FlowNode#getParents} + *
  • {@code startId} for {@link BlockEndNode#getStartNode} (otherwise absent) + *
+ * @see AnnotatedLogAction#annotateHtml + */ +@Restricted(NoExternalUse.class) +public class NewNodeConsoleNote extends ConsoleNote> { + + /** + * Prefix used in metadata lines. + */ + private static final String CONSOLE_NOTE_PREFIX = "[Pipeline] "; + + public static void print(FlowNode node, TaskListener listener) { + try { + listener.annotate(new NewNodeConsoleNote(node)); + } catch (IOException x) { + // never mind + } + listener.getLogger().println(CONSOLE_NOTE_PREFIX + node.getDisplayFunctionName()); + } + + private final @Nonnull String id; + private final @Nonnull String[] parents; + private final @CheckForNull String start; + + private NewNodeConsoleNote(FlowNode node) { + id = node.getId(); + List parentNodes = node.getParents(); + parents = new String[parentNodes.size()]; + for (int i = 0; i < parentNodes.size(); i++) { + parents[i] = parentNodes.get(i).getId(); + } + start = node instanceof BlockEndNode ? ((BlockEndNode) node).getStartNode().getId() : null; + } + + @Override + public ConsoleAnnotator annotate(Run context, MarkupText text, int charPos) { + StringBuilder startTag = new StringBuilder(""); + text.addMarkup(0, text.length(), startTag.toString(), ""); + // TODO should we also add another span around the actual displayFunctionName text, to make it easy to parse out? + return null; + } + + private static final long serialVersionUID = 1L; + + @Extension public static final class DescriptorImpl extends ConsoleAnnotationDescriptor {} + +} diff --git a/src/main/java/org/jenkinsci/plugins/workflow/job/console/PipelineLargeText.java b/src/main/java/org/jenkinsci/plugins/workflow/job/console/PipelineLargeText.java index bd09210a..7f732ea4 100644 --- a/src/main/java/org/jenkinsci/plugins/workflow/job/console/PipelineLargeText.java +++ b/src/main/java/org/jenkinsci/plugins/workflow/job/console/PipelineLargeText.java @@ -25,16 +25,40 @@ package org.jenkinsci.plugins.workflow.job.console; import com.google.common.base.Charsets; +import com.jcraft.jzlib.GZIPInputStream; +import com.jcraft.jzlib.GZIPOutputStream; +import com.trilead.ssh2.crypto.Base64; import hudson.console.AnnotatedLargeText; +import hudson.console.ConsoleAnnotationOutputStream; +import hudson.console.ConsoleAnnotator; +import hudson.remoting.ClassFilter; +import hudson.remoting.ObjectInputStreamEx; +import hudson.util.TimeUnit2; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Writer; +import static java.lang.Math.abs; import java.util.logging.Level; import java.util.logging.Logger; +import javax.crypto.Cipher; +import javax.crypto.CipherInputStream; +import javax.crypto.CipherOutputStream; +import jenkins.model.Jenkins; +import jenkins.security.CryptoConfidentialKey; +import org.apache.commons.io.IOUtils; +import org.apache.commons.io.input.CountingInputStream; +import org.apache.commons.io.output.ByteArrayOutputStream; import org.jenkinsci.plugins.workflow.flow.FlowExecutionOwner; import org.jenkinsci.plugins.workflow.job.WorkflowRun; import org.jenkinsci.plugins.workflow.support.actions.AnnotatedLogAction; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.stapler.Stapler; +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; import org.kohsuke.stapler.framework.io.ByteBuffer; /** @@ -43,25 +67,90 @@ @Restricted(NoExternalUse.class) public class PipelineLargeText extends AnnotatedLargeText { + private final WorkflowRun context; + public PipelineLargeText(WorkflowRun build) { - this(build, new ByteBuffer()); + this(build, new HackedByteBuffer()); + } + + /** Records length of the raw log file, so that {@link #doProgressText} does not think we have blown past the end. */ + static class HackedByteBuffer extends ByteBuffer { + long length; + @Override public long length() { + return Math.max(length, super.length()); + } } - private PipelineLargeText(WorkflowRun build, ByteBuffer buf) { + private PipelineLargeText(WorkflowRun build, HackedByteBuffer buf) { super(buf, Charsets.UTF_8, !build.isLogUpdated(), build); - // Overriding getLogTo works to strip annotations from plain-text console output. - // It does *not* work to override writeHtmlTo: - // AbstractMarkupText.wrapBy and similar routinely put the close tag on the next line, - // since the marked-up text includes the newline. - // Thus we would be trying to strip, e.g. "Some headline\n¦123Regular line\n". + // TODO for simplicitly, currently just making a copy of the log into a memory buffer. + // Overriding writeLogTo would work to strip annotations from plain-text console output more efficiently, + // though it would be cumbersome to also override all the other LargeText methods, esp. doProgressText. + // (We could also override ByteBuffer to stream output after stripping, but the length would be wrong, if anyone cares.) FlowExecutionOwner owner = build.asFlowExecutionOwner(); - if (owner != null) { - try (InputStream log = owner.getLog()) { - AnnotatedLogAction.strip(log, buf); - } catch (IOException ex) { - Logger.getLogger(PipelineLargeText.class.getName()).log(Level.SEVERE, null, ex); + assert owner != null; + try (InputStream log = owner.getLog(); CountingInputStream cis = new CountingInputStream(log)) { + AnnotatedLogAction.strip(cis, buf); + buf.length = cis.getByteCount(); + } catch (IOException ex) { + Logger.getLogger(PipelineLargeText.class.getName()).log(Level.SEVERE, null, ex); + } + this.context = build; + } + + // It does *not* work to override writeHtmlTo to strip node annotations after ConsoleNote’s are processed: + // AbstractMarkupText.wrapBy and similar routinely put the close tag on the next line, + // since the marked-up text includes the newline. + // Thus we would be trying to parse, e.g., "123¦Some headline\n123¦Regular line\n" + // and it is not necessarily obvious where the boundaries of the ID are. + // Anyway AnnotatedLogAction.annotateHtml is an easier way of handling node annotations. + @Override public long writeHtmlTo(long start, Writer w) throws IOException { + ConsoleAnnotationOutputStream caw = AnnotatedLogAction.annotateHtml( + w, createAnnotator(Stapler.getCurrentRequest()), context); + FlowExecutionOwner owner = context.asFlowExecutionOwner(); + assert owner != null; + long r; + try (InputStream log = owner.getLog()) { + log.skip(start); // TODO probably want to let the implementation of PipelineLogFile implement this more efficiently + CountingInputStream cis = new CountingInputStream(log); + IOUtils.copy(cis, caw); + r = start + cis.getByteCount(); + } + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + Cipher sym = PASSING_ANNOTATOR.encrypt(); + try (ObjectOutputStream oos = new ObjectOutputStream(new GZIPOutputStream(new CipherOutputStream(baos, sym)))) { + oos.writeLong(System.currentTimeMillis()); + oos.writeObject(caw.getConsoleAnnotator()); + } + StaplerResponse rsp = Stapler.getCurrentResponse(); + if (rsp != null) { + rsp.setHeader("X-ConsoleAnnotator", new String(Base64.encode(baos.toByteArray()))); + } + return r; + } + + private ConsoleAnnotator createAnnotator(StaplerRequest req) throws IOException { + try { + String base64 = req != null ? req.getHeader("X-ConsoleAnnotator") : null; + if (base64 != null) { + Cipher sym = PASSING_ANNOTATOR.decrypt(); + try (ObjectInputStream ois = new ObjectInputStreamEx(new GZIPInputStream( + new CipherInputStream(new ByteArrayInputStream(Base64.decode(base64.toCharArray())), sym)), + Jenkins.getInstance().pluginManager.uberClassLoader, + ClassFilter.DEFAULT)) { + long timestamp = ois.readLong(); + if (TimeUnit2.HOURS.toMillis(1) > abs(System.currentTimeMillis() - timestamp)) { + @SuppressWarnings("unchecked") ConsoleAnnotator annotator = (ConsoleAnnotator) ois.readObject(); + return annotator; + } + } } + } catch (ClassNotFoundException e) { + throw new IOException(e); } + return ConsoleAnnotator.initial(context); } + private static final CryptoConfidentialKey PASSING_ANNOTATOR = new CryptoConfidentialKey(PipelineLargeText.class, "consoleAnnotator"); + } diff --git a/src/main/java/org/jenkinsci/plugins/workflow/job/console/WorkflowRunConsoleNote.java b/src/main/java/org/jenkinsci/plugins/workflow/job/console/WorkflowRunConsoleNote.java index 48e83cef..11306200 100644 --- a/src/main/java/org/jenkinsci/plugins/workflow/job/console/WorkflowRunConsoleNote.java +++ b/src/main/java/org/jenkinsci/plugins/workflow/job/console/WorkflowRunConsoleNote.java @@ -24,22 +24,24 @@ package org.jenkinsci.plugins.workflow.job.console; +import org.jenkinsci.plugins.workflow.job.WorkflowRun; + import hudson.MarkupText; import hudson.console.ConsoleAnnotator; import hudson.console.ConsoleNote; import hudson.model.Run; -import hudson.model.TaskListener; -import java.io.IOException; /** - * Console note for Workflow metadata specific messages. + * @deprecated No longer used, but retained for serial-form compatibility of old build logs. + * @see NewNodeConsoleNote */ +@Deprecated public class WorkflowRunConsoleNote extends ConsoleNote> { /** * Prefix used in metadata lines. */ - private static final String CONSOLE_NOTE_PREFIX = "[Pipeline] "; + public static final String CONSOLE_NOTE_PREFIX = "[Pipeline] "; /** * CSS color selector. @@ -49,20 +51,13 @@ public class WorkflowRunConsoleNote extends ConsoleNote> { private static final String START_NOTE = ""; private static final String END_NOTE = ""; - public static void print(String message, TaskListener listener) { - try { - listener.annotate(new WorkflowRunConsoleNote()); - } catch (IOException x) { - // never mind - } - listener.getLogger().println(CONSOLE_NOTE_PREFIX + message); - } - - private WorkflowRunConsoleNote() {} - @Override - public ConsoleAnnotator annotate(Run context, MarkupText text, int charPos) { - text.addMarkup(0, text.length(), START_NOTE, END_NOTE); + public ConsoleAnnotator> annotate(Run context, MarkupText text, int charPos) { + if (context instanceof WorkflowRun) { + if (text.getText().startsWith(CONSOLE_NOTE_PREFIX)) { + text.addMarkup(0, text.length(), START_NOTE, END_NOTE); + } + } return null; } diff --git a/src/main/resources/org/jenkinsci/plugins/workflow/job/WorkflowRun/sidepanel.jelly b/src/main/resources/org/jenkinsci/plugins/workflow/job/WorkflowRun/sidepanel.jelly index ec33ee36..02840892 100644 --- a/src/main/resources/org/jenkinsci/plugins/workflow/job/WorkflowRun/sidepanel.jelly +++ b/src/main/resources/org/jenkinsci/plugins/workflow/job/WorkflowRun/sidepanel.jelly @@ -31,7 +31,17 @@ - + + + + + + + + + + + diff --git a/src/main/resources/org/jenkinsci/plugins/workflow/job/console/NewNodeConsoleNote/script.js b/src/main/resources/org/jenkinsci/plugins/workflow/job/console/NewNodeConsoleNote/script.js new file mode 100644 index 00000000..fdea5e53 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/workflow/job/console/NewNodeConsoleNote/script.js @@ -0,0 +1,29 @@ +Behaviour.specify("span.pipeline-new-node", 'NewNodeConsoleNote', 0, function(e) { + if (e.getAttribute('startId') == null) { + e.innerHTML = e.innerHTML.replace(/.+/, '$& (hide)') + } +}); + +function showHidePipelineSection(link) { + var span = link.parentNode + var id = span.getAttribute('nodeId') + var display + if (link.textContent === 'hide') { + display = 'none' + link.textContent = 'show' + } else { + display = 'inline' + link.textContent = 'hide' + } + // TODO for a block node, look up other pipeline-new-node elements with parentIds including this (transitively) and mask them and their text too + var sect = '.pipeline-node-' + id + var ss = document.styleSheets[0] + for (var i = 0; i < ss.rules.length; i++) { + if (ss.rules[i].selectorText === sect) { + ss.rules[i].style.display = display + return + } + } + // TODO order rules, so that hiding and reshowing a high-level section will restore expansion of a lower-level section + ss.insertRule(sect + ' {display: ' + display + '}', ss.rules.length) +} diff --git a/src/main/resources/org/jenkinsci/plugins/workflow/job/console/NewNodeConsoleNote/style.css b/src/main/resources/org/jenkinsci/plugins/workflow/job/console/NewNodeConsoleNote/style.css new file mode 100644 index 00000000..f081289a --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/workflow/job/console/NewNodeConsoleNote/style.css @@ -0,0 +1,3 @@ +span.pipeline-new-node { + color: #9A9999 +} diff --git a/src/test/java/org/jenkinsci/plugins/workflow/job/WorkflowRunTest.java b/src/test/java/org/jenkinsci/plugins/workflow/job/WorkflowRunTest.java index d6e680ef..7c807c9f 100644 --- a/src/test/java/org/jenkinsci/plugins/workflow/job/WorkflowRunTest.java +++ b/src/test/java/org/jenkinsci/plugins/workflow/job/WorkflowRunTest.java @@ -24,21 +24,14 @@ package org.jenkinsci.plugins.workflow.job; -import com.gargoylesoftware.htmlunit.html.HtmlPage; -import com.google.common.collect.Lists; -import hudson.console.ModelHyperlinkNote; import hudson.model.BallColor; -import hudson.model.Cause; -import hudson.model.CauseAction; import hudson.model.Executor; import hudson.model.Item; import hudson.model.ParametersAction; import hudson.model.ParametersDefinitionProperty; import hudson.model.Result; -import hudson.model.Run; import hudson.model.StringParameterDefinition; import hudson.model.StringParameterValue; -import hudson.model.TaskListener; import hudson.model.User; import hudson.model.queue.QueueTaskFuture; import hudson.security.ACL; @@ -53,38 +46,24 @@ import jenkins.model.CauseOfInterruption; import jenkins.model.InterruptedBuildAction; import jenkins.model.Jenkins; -import jenkins.security.NotReallyRoleSensitiveCallable; import org.apache.commons.io.FileUtils; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.not; import org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval; -import org.jenkinsci.plugins.workflow.actions.LogAction; import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; import org.jenkinsci.plugins.workflow.cps.CpsFlowExecution; import org.jenkinsci.plugins.workflow.cps.nodes.StepNode; import org.jenkinsci.plugins.workflow.flow.FlowExecution; import org.jenkinsci.plugins.workflow.graph.FlowGraphWalker; import org.jenkinsci.plugins.workflow.graph.FlowNode; -import org.jenkinsci.plugins.workflow.graphanalysis.DepthFirstScanner; -import org.jenkinsci.plugins.workflow.graphanalysis.FlowScanningUtils; -import org.jenkinsci.plugins.workflow.steps.AbstractStepDescriptorImpl; -import org.jenkinsci.plugins.workflow.steps.AbstractStepImpl; -import org.jenkinsci.plugins.workflow.steps.AbstractSynchronousStepExecution; -import org.jenkinsci.plugins.workflow.steps.StepContextParameter; -import org.jenkinsci.plugins.workflow.support.actions.AnnotatedLogAction; import org.jenkinsci.plugins.workflow.test.steps.SemaphoreStep; import static org.junit.Assert.*; import org.junit.ClassRule; -import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.jvnet.hudson.test.BuildWatcher; import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.MockAuthorizationStrategy; -import org.jvnet.hudson.test.TestExtension; import org.jvnet.hudson.test.recipes.LocalData; -import org.kohsuke.stapler.DataBoundConstructor; public class WorkflowRunTest { @@ -297,82 +276,4 @@ public void failedToStartRun() throws Exception { assertEquals(Collections.emptyList(), iba.getCauses()); } - @Issue("JENKINS-38381") - @Test public void consoleNotes() throws Exception { - r.jenkins.setSecurityRealm(r.createDummySecurityRealm()); - WorkflowJob p = r.createProject(WorkflowJob.class, "p"); - p.setDefinition(new CpsFlowDefinition("hyperlink()", true)); - User alice = User.get("alice"); - WorkflowRun b = r.assertBuildStatusSuccess(p.scheduleBuild2(0, new CauseAction(ACL.impersonate(alice.impersonate(), new NotReallyRoleSensitiveCallable() { - @Override public Cause call() throws RuntimeException { - return new Cause.UserIdCause(); - } - })))); - HtmlPage page = r.createWebClient().goTo(b.getUrl() + "console"); - assertLogContains(page, hudson.model.Messages.Cause_UserIdCause_ShortDescription(alice.getDisplayName()), alice.getUrl()); - assertLogContains(page, "Running inside " + b.getDisplayName(), b.getUrl()); - assertThat(page.getWebResponse().getContentAsString(), containsString("Running inside")); - DepthFirstScanner scanner = new DepthFirstScanner(); - scanner.setup(b.getExecution().getCurrentHeads()); - List nodes = Lists.newArrayList(scanner.filter(FlowScanningUtils.hasActionPredicate(LogAction.class))); - assertEquals(1, nodes.size()); - page = r.createWebClient().goTo(nodes.get(0).getUrl() + nodes.get(0).getAction(LogAction.class).getUrlName()); - assertLogContains(page, "Running inside " + b.getDisplayName(), b.getUrl()); - r.assertLogContains("\nRunning inside " + b.getDisplayName(), b); - } - private void assertLogContains(HtmlPage page, String plainText, String url) throws Exception { - String html = page.getWebResponse().getContentAsString(); - assertThat(page.getUrl() + " looks OK as text:\n" + html, page.getDocumentElement().getTextContent(), containsString(plainText)); - String absUrl = r.contextPath + "/" + url; - assertNotNull("found " + absUrl + " in:\n" + html, page.getAnchorByHref(absUrl)); - assertThat(html, not(containsString(AnnotatedLogAction.NODE_ID_SEP))); - } - public static class HyperlinkingStep extends AbstractStepImpl { - @DataBoundConstructor public HyperlinkingStep() {} - public static class Execution extends AbstractSynchronousStepExecution { - @StepContextParameter Run run; - @StepContextParameter TaskListener listener; - @Override protected Void run() throws Exception { - listener.getLogger().println("Running inside " + ModelHyperlinkNote.encodeTo(run)); - return null; - } - } - @TestExtension("consoleNotes") public static class DescriptorImpl extends AbstractStepDescriptorImpl { - public DescriptorImpl() { - super(Execution.class); - } - @Override public String getFunctionName() { - return "hyperlink"; - } - } - } - - @Ignore("TODO currently not implemented") - @Test - @Issue({"JENKINS-26122", "JENKINS-28222"}) - public void parallelBranchLabels() throws Exception { - WorkflowJob p = r.jenkins.createProject(WorkflowJob.class, "p"); - p.setDefinition(new CpsFlowDefinition( - "parallel a: {\n" + - " echo 'a-outside-1'\n" + - " withEnv(['A=1']) {echo 'a-inside-1'}\n" + - " echo 'a-outside-2'\n" + - " withEnv(['A=1']) {echo 'a-inside-2'}\n" + - "}, b: {\n" + - " echo 'b-outside-1'\n" + - " withEnv(['B=1']) {echo 'b-inside-1'}\n" + - " echo 'b-outside-2'\n" + - " withEnv(['B=1']) {echo 'b-inside-2'}\n" + - "}", true)); - WorkflowRun b = r.assertBuildStatusSuccess(p.scheduleBuild2(0)); - r.assertLogContains("[a] a-outside-1", b); - r.assertLogContains("[b] b-outside-1", b); - r.assertLogContains("[a] a-inside-1", b); - r.assertLogContains("[b] b-inside-1", b); - r.assertLogContains("[a] a-outside-2", b); - r.assertLogContains("[b] b-outside-2", b); - r.assertLogContains("[a] a-inside-2", b); - r.assertLogContains("[b] b-inside-2", b); - } - } diff --git a/src/test/java/org/jenkinsci/plugins/workflow/job/console/PipelineLargeTextTest.java b/src/test/java/org/jenkinsci/plugins/workflow/job/console/PipelineLargeTextTest.java new file mode 100644 index 00000000..74486d43 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/workflow/job/console/PipelineLargeTextTest.java @@ -0,0 +1,119 @@ +/* + * The MIT License + * + * Copyright (c) 2016, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.jenkinsci.plugins.workflow.job.console; + +import org.jenkinsci.plugins.workflow.job.*; +import com.gargoylesoftware.htmlunit.html.HtmlPage; +import com.google.common.collect.Lists; +import hudson.console.ModelHyperlinkNote; +import hudson.model.Cause; +import hudson.model.CauseAction; +import hudson.model.Run; +import hudson.model.TaskListener; +import hudson.model.User; +import hudson.security.ACL; +import java.util.List; +import jenkins.security.NotReallyRoleSensitiveCallable; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.not; +import org.jenkinsci.plugins.workflow.actions.LogAction; +import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; +import org.jenkinsci.plugins.workflow.graph.FlowNode; +import org.jenkinsci.plugins.workflow.graphanalysis.DepthFirstScanner; +import org.jenkinsci.plugins.workflow.graphanalysis.FlowScanningUtils; +import org.jenkinsci.plugins.workflow.steps.AbstractStepDescriptorImpl; +import org.jenkinsci.plugins.workflow.steps.AbstractStepImpl; +import org.jenkinsci.plugins.workflow.steps.AbstractSynchronousStepExecution; +import org.jenkinsci.plugins.workflow.steps.StepContextParameter; +import org.jenkinsci.plugins.workflow.support.actions.AnnotatedLogAction; +import static org.junit.Assert.*; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.BuildWatcher; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.TestExtension; +import org.kohsuke.stapler.DataBoundConstructor; + +@Issue("JENKINS-38381") +public class PipelineLargeTextTest { + + @ClassRule public static BuildWatcher buildWatcher = new BuildWatcher(); + @Rule public JenkinsRule r = new JenkinsRule(); + + @Test public void consoleNotes() throws Exception { + r.jenkins.setSecurityRealm(r.createDummySecurityRealm()); + WorkflowJob p = r.createProject(WorkflowJob.class, "p"); + p.setDefinition(new CpsFlowDefinition("hyperlink()", true)); + User alice = User.get("alice"); + WorkflowRun b = r.assertBuildStatusSuccess(p.scheduleBuild2(0, new CauseAction(ACL.impersonate(alice.impersonate(), new NotReallyRoleSensitiveCallable() { + @Override public Cause call() throws RuntimeException { + return new Cause.UserIdCause(); + } + })))); + HtmlPage page = r.createWebClient().goTo(b.getUrl() + "console"); + assertLogContains(page, hudson.model.Messages.Cause_UserIdCause_ShortDescription(alice.getDisplayName()), alice.getUrl()); + assertLogContains(page, "Running inside " + b.getDisplayName(), b.getUrl()); + assertThat(page.getWebResponse().getContentAsString().replace("\r\n", "\n"), + containsString("[Pipeline] hyperlink\nRunning inside { + @StepContextParameter Run run; + @StepContextParameter TaskListener listener; + @Override protected Void run() throws Exception { + listener.getLogger().println("Running inside " + ModelHyperlinkNote.encodeTo(run)); + return null; + } + } + @TestExtension("consoleNotes") public static class DescriptorImpl extends AbstractStepDescriptorImpl { + public DescriptorImpl() { + super(Execution.class); + } + @Override public String getFunctionName() { + return "hyperlink"; + } + } + } + + // TODO figure out how to test doProgressText/Html on a running build + +}