package hudson.console; import com.gargoylesoftware.htmlunit.Page; import com.gargoylesoftware.htmlunit.TextPage; import com.gargoylesoftware.htmlunit.WebRequestSettings; 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 org.jvnet.hudson.test.Bug; import org.jvnet.hudson.test.HudsonTestCase; import org.jvnet.hudson.test.SequenceLock; import org.jvnet.hudson.test.SingleFileSCM; import org.jvnet.hudson.test.TestBuilder; import org.jvnet.hudson.test.TestExtension; 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; /** * @author Kohsuke Kawaguchi */ public class ConsoleAnnotatorTest extends HudsonTestCase { /** * Let the build complete, and see if stateless {@link ConsoleAnnotator} annotations happen as expected. */ @Bug(6031) public void testCompletedStatelessLogAnnotation() throws Exception { FreeStyleProject p = 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 = buildAndAssertSuccess(p); // make sure we see the annotation HtmlPage rsp = createWebClient().getPage(b, "console"); assertEquals(1,rsp.selectNodes("//B[@class='demo']").size()); // make sure raw console output doesn't include the garbage TextPage raw = (TextPage)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 assertEquals(3,rsp.asXml().split("ooo").length); } /** * Only annotates the first occurrence of "ooo". */ @TestExtension("testCompletedStatelessLogAnnotation") public static final ConsoleAnnotatorFactory DEMO_ANNOTATOR = new ConsoleAnnotatorFactory() { public ConsoleAnnotator newInstance(Object context) { return new DemoAnnotator(); } }; public static class DemoAnnotator extends ConsoleAnnotator<Object> { @Override public ConsoleAnnotator annotate(Object build, MarkupText text) { if (text.getText().equals("ooo\n")) { text.addMarkup(0,3,"<b class=demo>","</b>"); return null; } return this; } } @Bug(6034) public void testConsoleAnnotationFilterOut() throws Exception { FreeStyleProject p = 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 = buildAndAssertSuccess(p); // make sure we see the annotation HtmlPage rsp = createWebClient().getPage(b, "console"); assertEquals(1,rsp.selectNodes("//A[@href='http://infradna.com/']").size()); // make sure raw console output doesn't include the garbage TextPage raw = (TextPage)createWebClient().goTo(b.getUrl()+"consoleText","text/plain"); System.out.println(raw.getContent()); assertTrue(raw.getContent().contains("\nabc\ndef\n")); } class ProgressiveLogClient { WebClient wc; Run run; String consoleAnnotator; String start; private Page p; ProgressiveLogClient(WebClient wc, Run r) { this.wc = wc; this.run = r; } String next() throws IOException { WebRequestSettings req = new WebRequestSettings(new URL(getURL() + run.getUrl() + "/logText/progressiveHtml"+(start!=null?"?start="+start:""))); 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. */ public void testProgressiveOutput() throws Exception { final SequenceLock lock = new SequenceLock(); WebClient wc = createWebClient(); FreeStyleProject p = 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 assertBuildStatusSuccess(f); } @TestExtension("testProgressiveOutput") 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. */ public void testConsoleAnnotation() throws Exception { final SequenceLock lock = new SequenceLock(); WebClient wc = createWebClient(); FreeStyleProject p = 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 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 { public String getDisplayName() { return "Dollar mark"; } } } /** * script.js defined in the annotator needs to be incorporated into the console page. */ public void testScriptInclusion() throws Exception { FreeStyleProject p = createFreeStyleProject(); FreeStyleBuild b = buildAndAssertSuccess(p); HtmlPage html = 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("testScriptInclusion") public static final class DescriptorImpl extends ConsoleAnnotationDescriptor { public String getDisplayName() { return "just to include a script"; } } } @TestExtension("testScriptInclusion") public static final class JustToIncludeScriptAnnotator extends ConsoleAnnotatorFactory { public ConsoleAnnotator newInstance(Object context) { return null; } } /** * Makes sure '<', '&', are escaped. */ @Bug(5952) public void testEscape() throws Exception { FreeStyleProject p = 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 = buildAndAssertSuccess(p); HtmlPage html = createWebClient().getPage(b, "console"); String text = html.asText(); System.out.println(text); assertTrue(text.contains("<b>&</b>")); assertTrue(b.getLog().contains("<b>&</b>")); } /** * Makes sure that annotations in the polling output is handled correctly. */ public void testPollingOutput() throws Exception { FreeStyleProject p = createFreeStyleProject(); p.setScm(new PollingSCM()); SCMTrigger t = new SCMTrigger("@daily"); t.start(p,true); p.addTrigger(t); buildAndAssertSuccess(p); // poll now t.new Runner().run(); HtmlPage log = 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); } @Override public String getDisplayName() { return "Test SCM"; } } } }