package hudson.console; import com.gargoylesoftware.htmlunit.Page; import com.gargoylesoftware.htmlunit.TextPage; import com.gargoylesoftware.htmlunit.WebRequest; import com.gargoylesoftware.htmlunit.html.DomNodeUtil; import com.gargoylesoftware.htmlunit.html.HtmlPage; import hudson.FilePath; import hudson.Launcher; import hudson.MarkupText; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.BuildListener; import hudson.model.FreeStyleBuild; import hudson.model.FreeStyleProject; import hudson.model.Run; import hudson.model.TaskListener; import hudson.scm.PollingResult; import hudson.scm.PollingResult.Change; import hudson.scm.RepositoryBrowser; import hudson.scm.SCMDescriptor; import hudson.scm.SCMRevisionState; import hudson.triggers.SCMTrigger; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URL; import java.util.HashMap; import java.util.Map; import java.util.concurrent.Future; import static org.junit.Assert.*; import org.junit.Rule; import org.junit.Test; import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.SequenceLock; import org.jvnet.hudson.test.SingleFileSCM; import org.jvnet.hudson.test.TestBuilder; import org.jvnet.hudson.test.TestExtension; /** * @author Kohsuke Kawaguchi */ public class ConsoleAnnotatorTest { @Rule public JenkinsRule r = new JenkinsRule(); /** * Let the build complete, and see if stateless {@link ConsoleAnnotator} annotations happen as expected. */ @Issue("JENKINS-6031") @Test public void completedStatelessLogAnnotation() throws Exception { FreeStyleProject p = r.createFreeStyleProject(); p.getBuildersList().add(new TestBuilder() { public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException { listener.getLogger().println("---"); listener.getLogger().println("ooo"); listener.getLogger().println("ooo"); return true; } }); FreeStyleBuild b = r.buildAndAssertSuccess(p); // make sure we see the annotation HtmlPage rsp = r.createWebClient().getPage(b, "console"); assertEquals(1, DomNodeUtil.selectNodes(rsp, "//B[@class='demo']").size()); // make sure raw console output doesn't include the garbage TextPage raw = (TextPage)r.createWebClient().goTo(b.getUrl()+"consoleText","text/plain"); System.out.println(raw.getContent()); String nl = System.getProperty("line.separator"); assertTrue(raw.getContent().contains(nl+"---"+nl+"ooo"+nl+"ooo"+nl)); // there should be two 'ooo's String xml = rsp.asXml(); assertEquals(xml, 3, xml.split("ooo").length); } /** * Only annotates the first occurrence of "ooo". */ @TestExtension("completedStatelessLogAnnotation") public static final ConsoleAnnotatorFactory DEMO_ANNOTATOR = new ConsoleAnnotatorFactory() { public ConsoleAnnotator newInstance(Object context) { return new DemoAnnotator(); } }; public static class DemoAnnotator extends ConsoleAnnotator<Object> { private static final String ANNOTATE_TEXT = "ooo" + System.getProperty("line.separator"); @Override public ConsoleAnnotator annotate(Object build, MarkupText text) { if (text.getText().equals(ANNOTATE_TEXT)) { text.addMarkup(0,3,"<b class=demo>","</b>"); return null; } return this; } } @Issue("JENKINS-6034") @Test public void consoleAnnotationFilterOut() throws Exception { FreeStyleProject p = r.createFreeStyleProject(); p.getBuildersList().add(new TestBuilder() { public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException { listener.getLogger().print("abc\n"); listener.getLogger().print(HyperlinkNote.encodeTo("http://infradna.com/","def")+"\n"); return true; } }); FreeStyleBuild b = r.buildAndAssertSuccess(p); // make sure we see the annotation HtmlPage rsp = r.createWebClient().getPage(b, "console"); assertEquals(1, DomNodeUtil.selectNodes(rsp, "//A[@href='http://infradna.com/']").size()); // make sure raw console output doesn't include the garbage TextPage raw = (TextPage)r.createWebClient().goTo(b.getUrl()+"consoleText","text/plain"); System.out.println(raw.getContent()); assertTrue(raw.getContent().contains("\nabc\ndef\n")); } class ProgressiveLogClient { JenkinsRule.WebClient wc; Run run; String consoleAnnotator; String start; private Page p; ProgressiveLogClient(JenkinsRule.WebClient wc, Run r) { this.wc = wc; this.run = r; } String next() throws IOException { WebRequest req = new WebRequest(new URL(r.getURL() + run.getUrl() + "/logText/progressiveHtml"+(start!=null?"?start="+start:""))); req.setEncodingType(null); Map headers = new HashMap(); if (consoleAnnotator!=null) headers.put("X-ConsoleAnnotator",consoleAnnotator); req.setAdditionalHeaders(headers); p = wc.getPage(req); consoleAnnotator = p.getWebResponse().getResponseHeaderValue("X-ConsoleAnnotator"); start = p.getWebResponse().getResponseHeaderValue("X-Text-Size"); return p.getWebResponse().getContentAsString(); } } /** * Tests the progressive output by making sure that the state of {@link ConsoleAnnotator}s are * maintained across different progressiveLog calls. */ @Test public void progressiveOutput() throws Exception { final SequenceLock lock = new SequenceLock(); JenkinsRule.WebClient wc = r.createWebClient(); FreeStyleProject p = r.createFreeStyleProject(); p.getBuildersList().add(new TestBuilder() { public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException { lock.phase(0); // make sure the build is now properly started lock.phase(2); listener.getLogger().println("line1"); lock.phase(4); listener.getLogger().println("line2"); lock.phase(6); return true; } }); Future<FreeStyleBuild> f = p.scheduleBuild2(0); lock.phase(1); FreeStyleBuild b = p.getBuildByNumber(1); ProgressiveLogClient plc = new ProgressiveLogClient(wc,b); // the page should contain some output indicating the build has started why and etc. plc.next(); lock.phase(3); assertEquals("<b tag=1>line1</b>\r\n",plc.next()); // the new invocation should start from where the previous call left off lock.phase(5); assertEquals("<b tag=2>line2</b>\r\n",plc.next()); lock.done(); // should complete successfully r.assertBuildStatusSuccess(f); } @TestExtension("progressiveOutput") public static final ConsoleAnnotatorFactory STATEFUL_ANNOTATOR = new ConsoleAnnotatorFactory() { public ConsoleAnnotator newInstance(Object context) { return new StatefulAnnotator(); } }; public static class StatefulAnnotator extends ConsoleAnnotator<Object> { int n=1; public ConsoleAnnotator annotate(Object build, MarkupText text) { if (text.getText().startsWith("line")) text.addMarkup(0,5,"<b tag="+(n++)+">","</b>"); return this; } } /** * Place {@link ConsoleNote}s and make sure it works. */ @Test public void consoleAnnotation() throws Exception { final SequenceLock lock = new SequenceLock(); JenkinsRule.WebClient wc = r.createWebClient(); FreeStyleProject p = r.createFreeStyleProject(); p.getBuildersList().add(new TestBuilder() { public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException { lock.phase(0); // make sure the build is now properly started lock.phase(2); listener.getLogger().print("abc"); listener.annotate(new DollarMark()); listener.getLogger().println("def"); lock.phase(4); listener.getLogger().print("123"); listener.annotate(new DollarMark()); listener.getLogger().print("456"); listener.annotate(new DollarMark()); listener.getLogger().println("789"); lock.phase(6); return true; } }); Future<FreeStyleBuild> f = p.scheduleBuild2(0); // discard the initial header portion lock.phase(1); FreeStyleBuild b = p.getBuildByNumber(1); ProgressiveLogClient plc = new ProgressiveLogClient(wc,b); plc.next(); lock.phase(3); assertEquals("abc$$$def\r\n",plc.next()); lock.phase(5); assertEquals("123$$$456$$$789\r\n",plc.next()); lock.done(); // should complete successfully r.assertBuildStatusSuccess(f); } /** * Places a triple dollar mark at the specified position. */ public static final class DollarMark extends ConsoleNote<Object> { public ConsoleAnnotator annotate(Object context, MarkupText text, int charPos) { text.addMarkup(charPos,"$$$"); return null; } @TestExtension public static final class DescriptorImpl extends ConsoleAnnotationDescriptor {} } /** * script.js defined in the annotator needs to be incorporated into the console page. */ @Test public void scriptInclusion() throws Exception { FreeStyleProject p = r.createFreeStyleProject(); FreeStyleBuild b = r.buildAndAssertSuccess(p); HtmlPage html = r.createWebClient().getPage(b, "console"); // verify that there's an element inserted by the script assertNotNull(html.getElementById("inserted-by-test1")); assertNotNull(html.getElementById("inserted-by-test2")); } public static final class JustToIncludeScript extends ConsoleNote<Object> { public ConsoleAnnotator annotate(Object build, MarkupText text, int charPos) { return null; } @TestExtension("scriptInclusion") public static final class DescriptorImpl extends ConsoleAnnotationDescriptor {} } @TestExtension("scriptInclusion") public static final class JustToIncludeScriptAnnotator extends ConsoleAnnotatorFactory { public ConsoleAnnotator newInstance(Object context) { return null; } } /** * Makes sure '<', '&', are escaped. */ @Issue("JENKINS-5952") @Test public void escape() throws Exception { FreeStyleProject p = r.createFreeStyleProject(); p.getBuildersList().add(new TestBuilder() { @Override public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException { listener.getLogger().println("<b>&</b>"); return true; } }); FreeStyleBuild b = r.buildAndAssertSuccess(p); HtmlPage html = r.createWebClient().getPage(b, "console"); String text = html.asText(); System.out.println(text); assertTrue(text.contains("<b>&</b>")); assertTrue(JenkinsRule.getLog(b).contains("<b>&</b>")); } /** * Makes sure that annotations in the polling output is handled correctly. */ @Test public void pollingOutput() throws Exception { FreeStyleProject p = r.createFreeStyleProject(); p.setScm(new PollingSCM()); SCMTrigger t = new SCMTrigger("@daily"); t.start(p,true); p.addTrigger(t); r.buildAndAssertSuccess(p); // poll now t.new Runner().run(); HtmlPage log = r.createWebClient().getPage(p, "scmPollLog"); String text = log.asText(); assertTrue(text, text.contains("$$$hello from polling")); } public static class PollingSCM extends SingleFileSCM { public PollingSCM() throws UnsupportedEncodingException { super("abc", "def"); } @Override protected PollingResult compareRemoteRevisionWith(AbstractProject project, Launcher launcher, FilePath workspace, TaskListener listener, SCMRevisionState baseline) throws IOException, InterruptedException { listener.annotate(new DollarMark()); listener.getLogger().println("hello from polling"); return new PollingResult(Change.NONE); } @TestExtension public static final class DescriptorImpl extends SCMDescriptor<PollingSCM> { public DescriptorImpl() { super(PollingSCM.class, RepositoryBrowser.class); } } } }