/** * MIT License * Copyright (c) 2013, Ericsson * * 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 hudson.plugins.throttleconcurrents; import com.gargoylesoftware.htmlunit.ElementNotFoundException; import com.gargoylesoftware.htmlunit.html.HtmlButton; import com.gargoylesoftware.htmlunit.html.HtmlElement; import com.gargoylesoftware.htmlunit.html.HtmlForm; import com.gargoylesoftware.htmlunit.html.HtmlInput; import com.gargoylesoftware.htmlunit.html.HtmlOption; import com.gargoylesoftware.htmlunit.html.HtmlPage; import com.gargoylesoftware.htmlunit.html.HtmlRadioButtonInput; import com.gargoylesoftware.htmlunit.html.HtmlSelect; import hudson.model.FreeStyleProject; import hudson.plugins.throttleconcurrents.testutils.HtmlUnitHelper; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import org.jvnet.hudson.test.HudsonTestCase; /** * This class initiates the testing of {@link hudson.plugins.throttleconcurrents.ThrottleQueueTaskDispatcher}.<br> * -Test methods for {@link hudson.plugins.throttleconcurrents.ThrottleQueueTaskDispatcher#canTake(hudson.model.Node, hudson.model.Queue.Task)}.<br> * -Happens to test {@link hudson.plugins.throttleconcurrents.ThrottleQueueTaskDispatcher#getMaxConcurrentPerNodeBasedOnMatchingLabels(hudson.model.Node, hudson.plugins.throttleconcurrents.ThrottleJobProperty.ThrottleCategory, int)}. * @author marco.miller@ericsson.com */ public class ThrottleQueueTaskDispatcherTest extends HudsonTestCase { private static final String buttonsXPath = "//button[@tabindex='0']"; private static final String configFormName = "config"; private static final String configUrlSuffix = "configure"; private static final String logUrlPrefix = "log/"; private static final String match = "match"; private static final String matchTrace = "node labels match"; private static final String max = "max"; private static final String maxTrace = "=> maxConcurrentPerNode' = "; private static final String mismatch = "mismatch"; private static final String mismatchTrace = "node labels mismatch"; private static final String parentXPath = "//td[contains(text(),'Throttl')]/.."; private static final String saveButtonText = "Save"; private static final String testCategoryName = "cat1"; private static final String testCategoryLabel = testCategoryName+"label"; // private static final boolean configureNodeLabel = true; private static final boolean configureNoNodeLabel = false; private static final boolean expectMatch = true; private static final boolean expectMismatch = false; // private static final int configureOneMaxLabelPair = 1; private static final int configureTwoMaxLabelPairs = 2; private static final int noCategoryWideMaxConcurrentPerNode = 0; private static final int someCategoryWideMaxConcurrentPerNode = 1; private static final int greaterCategoryWideMaxConcurrentPerNode = configureOneMaxLabelPair+1; @Override protected void setUp() throws Exception { super.setUp(); } @Override protected void tearDown() throws Exception { super.tearDown(); } /** * @throws ExecutionException upon Jenkins project build scheduling issue. * @throws InterruptedException upon Jenkins global configuration issue. * @throws IOException upon many potential Jenkins IO issues during test. */ public void testShouldConsiderTaskAsBlockableStillUponMatchingMaxLabelPair() throws ExecutionException, InterruptedException, IOException { assertBasedOnMaxLabelPairMatchingOrNot( configureOneMaxLabelPair, noCategoryWideMaxConcurrentPerNode, expectMatch, configureNodeLabel); } /** * @throws ExecutionException upon Jenkins project build scheduling issue. * @throws InterruptedException upon Jenkins global configuration issue. * @throws IOException upon many potential Jenkins IO issues during test. */ public void testShouldConsiderTaskAsBlockableStillUponMatchingMaxLabelPairs() throws ExecutionException, InterruptedException, IOException { assertBasedOnMaxLabelPairMatchingOrNot( configureTwoMaxLabelPairs, noCategoryWideMaxConcurrentPerNode, expectMatch, configureNodeLabel); } /** * @throws ExecutionException upon Jenkins project build scheduling issue. * @throws InterruptedException upon Jenkins global configuration issue. * @throws IOException upon many potential Jenkins IO issues during test. */ public void testShouldConsiderTaskAsBlockableStillUponMatchingLabelPairWithLowestMax() throws ExecutionException, InterruptedException, IOException { assertBasedOnMaxLabelPairMatchingOrNot( configureOneMaxLabelPair, //=> label-pair max of 1, still to match as *the* max; greaterCategoryWideMaxConcurrentPerNode, //greater than label-pair max but still expectMatch, configureNodeLabel); } /** * @throws ExecutionException upon Jenkins project build scheduling issue. * @throws InterruptedException upon Jenkins global configuration issue. * @throws IOException upon many potential Jenkins IO issues during test. */ public void testShouldConsiderTaskAsBuildableStillUponMismatchingMaxLabelPairs() throws ExecutionException, InterruptedException, IOException { assertBasedOnMaxLabelPairMatchingOrNot( configureTwoMaxLabelPairs, someCategoryWideMaxConcurrentPerNode, expectMismatch, configureNodeLabel); } /** * @throws ExecutionException upon Jenkins project build scheduling issue. * @throws InterruptedException upon Jenkins global configuration issue. * @throws IOException upon many potential Jenkins IO issues during test. */ public void testShouldConsiderTaskAsBuildableStillUponNoNodeLabel() throws ExecutionException, InterruptedException, IOException { assertBasedOnMaxLabelPairMatchingOrNot( configureOneMaxLabelPair, someCategoryWideMaxConcurrentPerNode, expectMismatch, configureNoNodeLabel); } /** * @param targetedPairNumber of throttling category maximum/label pairs. * @param maxConcurrentPerNode or category-wide maximum. * @param expectMatch of labels (or not). * @param configureNodeLabel or not. * @throws ExecutionException upon Jenkins project build scheduling issue. * @throws InterruptedException upon Jenkins global configuration issue. * @throws IOException upon many potential Jenkins IO issues during test. */ private void assertBasedOnMaxLabelPairMatchingOrNot( int targetedPairNumber, int maxConcurrentPerNode, boolean expectMatch, boolean configureNodeLabel) throws ExecutionException, InterruptedException, IOException { if(configureNodeLabel) { String nodeLabelSuffix = expectMatch ? "" : "other"; configureNewNodeWithLabel(testCategoryLabel +targetedPairNumber +nodeLabelSuffix); } configureGlobalThrottling(testCategoryLabel, targetedPairNumber, maxConcurrentPerNode); FreeStyleProject project = createFreeStyleProject(); configureJobThrottling(project); String logger = configureLogger(); project.scheduleBuild2(0).get(); HtmlPage page = getLoggerPage(logger); if(expectMatch) { assertTrue(expectedTracesMessage(match, true), page.asText().contains(matchTrace)); assertTrue(expectedTracesMessage(max, true), page.asText().contains(maxTrace+targetedPairNumber)); } else { assertTrue(expectedTracesMessage(mismatch, true), page.asText().contains(mismatchTrace)); assertFalse(expectedTracesMessage(max, false), page.asText().contains(maxTrace)); } } private void configureGlobalThrottling(String labelRoot, int numberOfPairs, int maxConcurrentPerNode) throws InterruptedException, IOException, MalformedURLException { URL url = new URL(getURL()+configUrlSuffix); HtmlPage page = createWebClient().getPage(url); HtmlForm form = page.getFormByName(configFormName); List<HtmlButton> buttons = HtmlUnitHelper.getButtonsByXPath(form, parentXPath+buttonsXPath); String buttonText = "Add Category"; boolean buttonFound = false; for(HtmlButton button : buttons) { if(button.getTextContent().equals(buttonText)) { buttonFound = true; button.click(); HtmlInput input = form.getInputByName("_.categoryName"); input.setValueAttribute(testCategoryName); //_.maxConcurrentTotal ignored. input = form.getInputByName("_.maxConcurrentPerNode"); input.setValueAttribute(""+maxConcurrentPerNode); buttons = HtmlUnitHelper.getButtonsByXPath(form, parentXPath+buttonsXPath); buttonText = "Add Maximum Per Labeled Node"; buttonFound = false; for(HtmlButton deeperButton: buttons) { if(deeperButton.getTextContent().equals(buttonText)) { buttonFound = true; for(int i=0; i<numberOfPairs; i++) { List<HtmlInput> inputs = null; int clickThenWaitForMaxTries = 3; do { page = (HtmlPage)deeperButton.click(); TimeUnit.SECONDS.sleep(1); form = page.getFormByName(configFormName); inputs = form.getInputsByName("_.throttledNodeLabel"); clickThenWaitForMaxTries--; } while(inputs.isEmpty() && clickThenWaitForMaxTries > 0); assertFalse(buttonText+" button clicked; no resulting field found on "+url, inputs.isEmpty()); inputs.get(i).setValueAttribute(labelRoot+(i+1)); inputs = form.getInputsByName("_.maxConcurrentPerNodeLabeled"); inputs.get(i).setValueAttribute(""+(i+1)); } } } failWithMessageIfButtonNotFoundOnPage(buttonFound, buttonText, url); break; } } failWithMessageIfButtonNotFoundOnPage(buttonFound, buttonText, url); buttons = HtmlUnitHelper.getButtonsByXPath(form, buttonsXPath); buttonText = saveButtonText; buttonFound = buttonFoundThusFormSubmitted(form, buttons, buttonText); failWithMessageIfButtonNotFoundOnPage(buttonFound, buttonText, url); } private void configureJobThrottling(FreeStyleProject project) throws IOException, MalformedURLException { URL url = new URL(getURL()+project.getUrl()+configUrlSuffix); HtmlPage page = createWebClient().getPage(url); HtmlForm form = page.getFormByName(configFormName); List<HtmlButton> buttons = HtmlUnitHelper.getButtonsByXPath(form, buttonsXPath); String buttonText = saveButtonText; boolean buttonFound = false; for(HtmlButton button: buttons) { if(button.getTextContent().equals(buttonText)) { buttonFound = true; String checkboxName = "throttleEnabled"; HtmlElement checkbox = page.getElementByName(checkboxName); assertNotNull(checkboxName+" checkbox not found on test job config page; plugin installed?", checkbox); checkbox.click(); List<HtmlRadioButtonInput> radios = form.getRadioButtonsByName("throttleOption"); for(HtmlRadioButtonInput radio: radios) { radio.setChecked(radio.getValueAttribute().equals("category")); } checkbox = page.getElementByName("categories"); checkbox.click(); button.click(); break; } } failWithMessageIfButtonNotFoundOnPage(buttonFound, buttonText, url); } private void configureNewNodeWithLabel(String label) throws IOException, MalformedURLException { URL url = new URL(getURL()+"computer/new"); HtmlPage page = createWebClient().getPage(url); HtmlForm form = page.getFormByName("createItem"); HtmlInput input = form.getInputByName("name"); input.setValueAttribute("test"); List<HtmlRadioButtonInput> radios = form.getRadioButtonsByName("mode"); for(HtmlRadioButtonInput radio: radios) { radio.setChecked(radio.getValueAttribute().equals("hudson.slaves.DumbSlave")); } List<HtmlButton> buttons = HtmlUnitHelper.getButtonsByXPath(form, buttonsXPath); String buttonText = "OK"; boolean buttonFound = false; for(HtmlButton button: buttons) { if(button.getTextContent().equals(buttonText)) { buttonFound = true; page = button.click(); List<HtmlForm> forms = page.getForms(); for(HtmlForm aForm: forms) { if(aForm.getActionAttribute().equals("doCreateItem")) { form = aForm; break; } } input = form.getInputByName("_.numExecutors"); input.setValueAttribute("1"); input = form.getInputByName("_.remoteFS"); input.setValueAttribute("/"); input = form.getInputByName("_.labelString"); input.setValueAttribute(label); break; } } failWithMessageIfButtonNotFoundOnPage(buttonFound, buttonText, url); buttons = HtmlUnitHelper.getButtonsByXPath(form, buttonsXPath); buttonText = saveButtonText; buttonFound = buttonFoundThusFormSubmitted(form, buttons, buttonText); failWithMessageIfButtonNotFoundOnPage(buttonFound, buttonText, url); } private String configureLogger() throws IOException, MalformedURLException { String logger = ThrottleQueueTaskDispatcher.class.getName(); jenkins.getLog().doNewLogRecorder(logger); URL url = new URL(getURL()+logUrlPrefix+logger+"/"+configUrlSuffix); HtmlPage page = createWebClient().getPage(url); HtmlForm form = page.getFormByName(configFormName); List<HtmlButton> buttons = HtmlUnitHelper.getButtonsByXPath(form, buttonsXPath); String buttonText = "Add"; boolean buttonFound = false; for(HtmlButton button: buttons) { if(button.getTextContent().equals(buttonText)) { buttonFound = true; button.click(); List<HtmlInput> inputs = form.getInputsByName("_.name"); for(HtmlInput input: inputs) { input.setValueAttribute(logger); } HtmlSelect select = form.getSelectByName("level"); HtmlOption option; try { option = select.getOptionByValue("fine"); } catch (ElementNotFoundException e) { // gets upper case since Jenkins 1.519 option = select.getOptionByValue("FINE"); } select.setSelectedAttribute(option, true); break; } } failWithMessageIfButtonNotFoundOnPage(buttonFound, buttonText, url); buttonText = saveButtonText; buttonFound = buttonFoundThusFormSubmitted(form, buttons, buttonText); failWithMessageIfButtonNotFoundOnPage(buttonFound, buttonText, url); return logger; } private boolean buttonFoundThusFormSubmitted(HtmlForm form, List<HtmlButton> buttons, String buttonText) throws IOException { boolean buttonFound = false; for(HtmlButton button: buttons) { if(button.getTextContent().equals(buttonText)) { buttonFound = true; button.click(); break; } } return buttonFound; } private String expectedTracesMessage(String traceKind, boolean assertingTrue) { StringBuffer messagePrefix = new StringBuffer("log shall"); if(!assertingTrue) { messagePrefix.append(" not"); } return messagePrefix+" contain '"+traceKind+"' traces"; } private void failWithMessageIfButtonNotFoundOnPage(boolean buttonFound, String buttonText, URL url) { assertTrue(buttonText+" button not found on "+url, buttonFound); } private HtmlPage getLoggerPage(String logger) throws IOException, MalformedURLException { URL url = new URL(getURL()+logUrlPrefix+logger); return createWebClient().getPage(url); } }