diff options
author | Sébastien Lesaint <sebastien.lesaint@sonarsource.com> | 2019-08-14 11:07:17 +0200 |
---|---|---|
committer | SonarTech <sonartech@sonarsource.com> | 2019-08-14 20:21:16 +0200 |
commit | 98efbbadb8df0754a72292011e8ad3178abf90f2 (patch) | |
tree | 9b230b3f9b06078e085e98d486b7222f94f4269e /server/sonar-webserver-core | |
parent | b4694fd3ae3ef3cd0e189883b490057447842441 (diff) | |
download | sonarqube-98efbbadb8df0754a72292011e8ad3178abf90f2.tar.gz sonarqube-98efbbadb8df0754a72292011e8ad3178abf90f2.zip |
rename sonar-server to sonar-webserver-core
Diffstat (limited to 'server/sonar-webserver-core')
199 files changed, 18484 insertions, 0 deletions
diff --git a/server/sonar-webserver-core/COPYING b/server/sonar-webserver-core/COPYING new file mode 100644 index 00000000000..fc8a5de7edf --- /dev/null +++ b/server/sonar-webserver-core/COPYING @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/server/sonar-webserver-core/build.gradle b/server/sonar-webserver-core/build.gradle new file mode 100644 index 00000000000..9ff120d6cfb --- /dev/null +++ b/server/sonar-webserver-core/build.gradle @@ -0,0 +1,81 @@ +sonarqube { + properties { + property 'sonar.projectName', "${projectTitle} :: Web Server :: Core" + } +} + +import org.apache.tools.ant.filters.ReplaceTokens +processResources { + filesMatching('build.properties') { + filter ReplaceTokens, tokens: [ + 'buildNumber': release ? 'git rev-parse HEAD'.execute().text.trim() : 'N/A' + ] + } +} + +dependencies { + // please keep the list grouped by configuration and ordered by name + + compile 'ch.qos.logback:logback-access' + compile 'ch.qos.logback:logback-classic' + compile 'ch.qos.logback:logback-core' + compile 'com.google.code.gson:gson' + compile 'com.google.protobuf:protobuf-java' + compile 'commons-dbutils:commons-dbutils' + compile 'io.jsonwebtoken:jjwt-api' + compile 'io.jsonwebtoken:jjwt-impl' + compile 'org.apache.httpcomponents:httpclient' + compile 'org.apache.logging.log4j:log4j-api' + compile 'org.apache.tomcat.embed:tomcat-embed-core' + compile 'org.apache.commons:commons-dbcp2' + compile 'org.picocontainer:picocontainer' + compile 'org.slf4j:jul-to-slf4j' + compile 'org.slf4j:slf4j-api' + compile 'org.sonarsource.update-center:sonar-update-center-common' + compile 'org.mindrot:jbcrypt' + + compile project(':server:sonar-ce-common') + compile project(':server:sonar-ce-task') + compile project(':server:sonar-ce-task-projectanalysis') + compile project(':server:sonar-db-dao') + compile project(':server:sonar-db-migration') + compile project(':server:sonar-process') + compile project(':server:sonar-server-common') + compile project(':server:sonar-webserver-auth') + compile project(':server:sonar-webserver-common') + compile project(':server:sonar-webserver-es') + compile project(':sonar-core') + compile project(':sonar-duplications') + compile project(':sonar-scanner-protocol') + compile project(':sonar-markdown') + compile project(path: ':sonar-plugin-api', configuration: 'shadow') + compile project(':sonar-plugin-api-impl') + compile project(':sonar-ws') + + compileOnly 'com.google.code.findbugs:jsr305' + // not a transitive dep. At runtime lib/jdbc/h2 is used + compileOnly 'com.h2database:h2' + + testCompile 'com.github.kevinsawicki:http-request' + testCompile 'com.google.code.findbugs:jsr305' + testCompile 'com.squareup.okhttp3:mockwebserver' + testCompile 'com.tngtech.java:junit-dataprovider' + testCompile 'junit:junit' + testCompile 'org.apache.logging.log4j:log4j-api' + testCompile 'org.apache.logging.log4j:log4j-core' + testCompile 'org.assertj:assertj-core' + testCompile 'org.assertj:assertj-guava' + testCompile 'org.eclipse.jetty:jetty-server' + testCompile 'org.eclipse.jetty:jetty-servlet' + testCompile 'org.hamcrest:hamcrest-all' + testCompile 'org.mockito:mockito-core' + testCompile 'org.subethamail:subethasmtp' + testCompile project(':server:sonar-db-testing') + testCompile project(path: ":server:sonar-server-common", configuration: "tests") + testCompile project(path: ":server:sonar-webserver-auth", configuration: "tests") + testCompile project(path: ":server:sonar-webserver-es", configuration: "tests") + testCompile project(path: ":server:sonar-webserver-ws", configuration: "tests") + testCompile project(':sonar-testing-harness') + + runtime 'io.jsonwebtoken:jjwt-jackson' +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/app/WebServerProcessLogging.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/app/WebServerProcessLogging.java new file mode 100644 index 00000000000..9f866b62d70 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/app/WebServerProcessLogging.java @@ -0,0 +1,57 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.app; + +import ch.qos.logback.classic.Level; +import org.sonar.process.ProcessId; +import org.sonar.process.logging.LogDomain; +import org.sonar.process.logging.LogLevelConfig; +import org.sonar.server.log.ServerProcessLogging; + +import static org.sonar.server.platform.web.requestid.RequestIdMDCStorage.HTTP_REQUEST_ID_MDC_KEY; + +/** + * Configure logback for the Web Server process. Logs are written to file "web.log" in SQ's log directory. + */ +public class WebServerProcessLogging extends ServerProcessLogging { + + public WebServerProcessLogging() { + super(ProcessId.WEB_SERVER, "%X{" + HTTP_REQUEST_ID_MDC_KEY + "}"); + } + + @Override + protected void extendLogLevelConfiguration(LogLevelConfig.Builder logLevelConfigBuilder) { + logLevelConfigBuilder.levelByDomain("sql", ProcessId.WEB_SERVER, LogDomain.SQL); + logLevelConfigBuilder.levelByDomain("es", ProcessId.WEB_SERVER, LogDomain.ES); + logLevelConfigBuilder.levelByDomain("auth.event", ProcessId.WEB_SERVER, LogDomain.AUTH_EVENT); + JMX_RMI_LOGGER_NAMES.forEach(loggerName -> logLevelConfigBuilder.levelByDomain(loggerName, ProcessId.WEB_SERVER, LogDomain.JMX)); + + logLevelConfigBuilder.offUnlessTrace("org.apache.catalina.core.ContainerBase"); + logLevelConfigBuilder.offUnlessTrace("org.apache.catalina.core.StandardContext"); + logLevelConfigBuilder.offUnlessTrace("org.apache.catalina.core.StandardService"); + + LOGGER_NAMES_TO_TURN_OFF.forEach(loggerName -> logLevelConfigBuilder.immutableLevel(loggerName, Level.OFF)); + } + + @Override + protected void extendConfigure() { + // No extension needed + } +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/debt/DebtModelPluginRepository.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/debt/DebtModelPluginRepository.java new file mode 100644 index 00000000000..016f20434b9 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/debt/DebtModelPluginRepository.java @@ -0,0 +1,140 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.debt; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.Maps; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import org.picocontainer.Startable; +import org.sonar.api.Plugin; +import org.sonar.api.server.ServerSide; +import org.sonar.core.platform.PluginInfo; +import org.sonar.core.platform.PluginRepository; + +import static com.google.common.collect.Lists.newArrayList; + +/** + * <p>This class is used to find which technical debt model XML files exist in the Sonar instance.</p> + * <p> + * Those XML files are provided by language plugins that embed their own contribution to the definition of the Technical debt model. + * They must be located in the classpath of those language plugins, more specifically in the "com.sonar.sqale" package, and + * they must be named "<pluginKey>-model.xml". + * </p> + */ +@ServerSide +public class DebtModelPluginRepository implements Startable { + + public static final String DEFAULT_MODEL = "technical-debt"; + + private static final String XML_FILE_SUFFIX = "-model.xml"; + private static final String XML_FILE_PREFIX = "com/sonar/sqale/"; + + private String xmlFilePrefix; + + private PluginRepository pluginRepository; + private Map<String, ClassLoader> contributingPluginKeyToClassLoader; + + public DebtModelPluginRepository(PluginRepository pluginRepository) { + this.pluginRepository = pluginRepository; + this.xmlFilePrefix = XML_FILE_PREFIX; + } + + @VisibleForTesting + DebtModelPluginRepository(PluginRepository pluginRepository, String xmlFilePrefix) { + this.pluginRepository = pluginRepository; + this.xmlFilePrefix = xmlFilePrefix; + } + + @VisibleForTesting + DebtModelPluginRepository(Map<String, ClassLoader> contributingPluginKeyToClassLoader, String xmlFilePrefix) { + this.contributingPluginKeyToClassLoader = contributingPluginKeyToClassLoader; + this.xmlFilePrefix = xmlFilePrefix; + } + + /** + * {@inheritDoc} + */ + @Override + public void start() { + findAvailableXMLFiles(); + } + + private void findAvailableXMLFiles() { + if (contributingPluginKeyToClassLoader == null) { + contributingPluginKeyToClassLoader = Maps.newTreeMap(); + // Add default model + contributingPluginKeyToClassLoader.put(DEFAULT_MODEL, getClass().getClassLoader()); + for (PluginInfo pluginInfo : pluginRepository.getPluginInfos()) { + String pluginKey = pluginInfo.getKey(); + Plugin plugin = pluginRepository.getPluginInstance(pluginKey); + ClassLoader classLoader = plugin.getClass().getClassLoader(); + if (classLoader.getResource(getXMLFilePath(pluginKey)) != null) { + contributingPluginKeyToClassLoader.put(pluginKey, classLoader); + } + } + } + contributingPluginKeyToClassLoader = Collections.unmodifiableMap(contributingPluginKeyToClassLoader); + } + + @VisibleForTesting + String getXMLFilePath(String model) { + return xmlFilePrefix + model + XML_FILE_SUFFIX; + } + + /** + * Returns the list of plugins that can contribute to the technical debt model. + * + * @return the list of plugin keys + */ + public Collection<String> getContributingPluginList() { + return newArrayList(contributingPluginKeyToClassLoader.keySet()); + } + + /** + * Creates a new {@link java.io.Reader} for the XML file that contains the model contributed by the given plugin. + * + * @param pluginKey the key of the plugin that contributes the XML file + * @return the reader, that must be closed once its use is finished. + */ + public Reader createReaderForXMLFile(String pluginKey) { + ClassLoader classLoader = contributingPluginKeyToClassLoader.get(pluginKey); + String xmlFilePath = getXMLFilePath(pluginKey); + return new InputStreamReader(classLoader.getResourceAsStream(xmlFilePath), StandardCharsets.UTF_8); + } + + @VisibleForTesting + Map<String, ClassLoader> getContributingPluginKeyToClassLoader() { + return contributingPluginKeyToClassLoader; + } + + /** + * {@inheritDoc} + */ + @Override + public void stop() { + // Nothing to do + } + +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/debt/DebtModelXMLExporter.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/debt/DebtModelXMLExporter.java new file mode 100644 index 00000000000..5536f1baf7f --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/debt/DebtModelXMLExporter.java @@ -0,0 +1,204 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.debt; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; +import javax.xml.XMLConstants; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Source; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.sax.SAXSource; +import javax.xml.transform.sax.SAXTransformerFactory; +import javax.xml.transform.stream.StreamResult; +import org.apache.commons.lang.StringEscapeUtils; +import org.apache.commons.lang.StringUtils; +import org.sonar.api.rule.RuleKey; +import org.sonar.api.server.ServerSide; +import org.xml.sax.InputSource; + +/** + * Export characteristics and rule debt definitions to XML + */ +@ServerSide +public class DebtModelXMLExporter { + + private static final String ROOT = "sqale"; + private static final String DEFAULT_INDENT = "2"; + + public static final String CHARACTERISTIC = "chc"; + public static final String PROPERTY = "prop"; + public static final String PROPERTY_KEY = "key"; + public static final String PROPERTY_VALUE = "val"; + public static final String PROPERTY_TEXT_VALUE = "txt"; + + public static final String REPOSITORY_KEY = "rule-repo"; + public static final String RULE_KEY = "rule-key"; + + public static final String PROPERTY_FUNCTION = "remediationFunction"; + public static final String PROPERTY_COEFFICIENT = "remediationFactor"; + public static final String PROPERTY_OFFSET = "offset"; + + protected String export(List<RuleDebt> allRules) { + StringBuilder sb = new StringBuilder(); + sb.append("<" + ROOT + ">"); + for (RuleDebt rule : allRules) { + processRule(rule, sb); + } + sb.append("</" + ROOT + ">"); + String xml = sb.toString(); + xml = prettyFormatXml(xml); + return xml; + } + + private static void processRule(RuleDebt rule, StringBuilder xml) { + xml.append("<" + CHARACTERISTIC + ">"); + xml.append("<" + REPOSITORY_KEY + ">"); + xml.append(StringEscapeUtils.escapeXml(rule.ruleKey().repository())); + xml.append("</" + REPOSITORY_KEY + "><" + RULE_KEY + ">"); + xml.append(StringEscapeUtils.escapeXml(rule.ruleKey().rule())); + xml.append("</" + RULE_KEY + ">"); + + String coefficient = rule.coefficient(); + String offset = rule.offset(); + + processProperty(PROPERTY_FUNCTION, null, rule.function(), xml); + if (coefficient != null) { + String[] values = getValues(coefficient); + processProperty(PROPERTY_COEFFICIENT, values[0], values[1], xml); + } + if (offset != null) { + String[] values = getValues(offset); + processProperty(PROPERTY_OFFSET, values[0], values[1], xml); + } + xml.append("</" + CHARACTERISTIC + ">"); + } + + private static String[] getValues(String factorOrOffset) { + String[] result = new String[2]; + Pattern pattern = Pattern.compile("(\\d+)(\\w+)"); + Matcher matcher = pattern.matcher(factorOrOffset); + if (matcher.find()) { + String value = matcher.group(1); + String unit = matcher.group(2); + result[0] = value; + result[1] = unit; + } + return result; + } + + private static void processProperty(String key, @Nullable String val, String text, StringBuilder xml) { + xml.append("<" + PROPERTY + "><" + PROPERTY_KEY + ">"); + xml.append(StringEscapeUtils.escapeXml(key)); + xml.append("</" + PROPERTY_KEY + ">"); + if (val != null) { + xml.append("<" + PROPERTY_VALUE + ">"); + xml.append(val); + xml.append("</" + PROPERTY_VALUE + ">"); + } + if (StringUtils.isNotEmpty(text)) { + xml.append("<" + PROPERTY_TEXT_VALUE + ">"); + xml.append(StringEscapeUtils.escapeXml(text)); + xml.append("</" + PROPERTY_TEXT_VALUE + ">"); + } + xml.append("</" + PROPERTY + ">"); + } + + private static String prettyFormatXml(String xml) { + try { + TransformerFactory factory = SAXTransformerFactory.newInstance(); + factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + Transformer serializer = factory.newTransformer(); + serializer.setOutputProperty(OutputKeys.INDENT, "yes"); + serializer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); + serializer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", DEFAULT_INDENT); + Source xmlSource = new SAXSource(new InputSource(new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)))); + StreamResult res = new StreamResult(new ByteArrayOutputStream()); + serializer.transform(xmlSource, res); + return new String(((ByteArrayOutputStream) res.getOutputStream()).toByteArray(), StandardCharsets.UTF_8); + } catch (TransformerException ignored) { + // Ignore, raw XML will be returned + } + return xml; + } + + public static class RuleDebt { + private RuleKey ruleKey; + private String function; + private String coefficient; + private String offset; + + public RuleKey ruleKey() { + return ruleKey; + } + + public RuleDebt setRuleKey(RuleKey ruleKey) { + this.ruleKey = ruleKey; + return this; + } + + public String function() { + return function; + } + + public RuleDebt setFunction(String function) { + this.function = function; + return this; + } + + @CheckForNull + public String coefficient() { + return coefficient; + } + + public RuleDebt setCoefficient(@Nullable String coefficient) { + this.coefficient = coefficient; + return this; + } + + @CheckForNull + public String offset() { + return offset; + } + + public RuleDebt setOffset(@Nullable String offset) { + this.offset = offset; + return this; + } + + @Override + public String toString() { + return "RuleDebt{" + + "ruleKey=" + ruleKey + + ", function=" + function + + ", coefficient='" + coefficient + '\'' + + ", offset='" + offset + '\'' + + '}'; + } + } +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/debt/DebtRulesXMLImporter.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/debt/DebtRulesXMLImporter.java new file mode 100644 index 00000000000..18d7dc1168d --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/debt/DebtRulesXMLImporter.java @@ -0,0 +1,272 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.debt; + +import com.google.common.base.Predicate; +import com.google.common.base.Strings; +import com.google.common.collect.Iterables; +import java.io.Reader; +import java.io.StringReader; +import java.util.List; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamException; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.math.NumberUtils; +import org.codehaus.stax2.XMLInputFactory2; +import org.codehaus.staxmate.SMInputFactory; +import org.codehaus.staxmate.in.SMHierarchicCursor; +import org.codehaus.staxmate.in.SMInputCursor; +import org.sonar.api.rule.RuleKey; +import org.sonar.api.server.ServerSide; +import org.sonar.api.utils.Duration; +import org.sonar.api.utils.ValidationMessages; +import org.sonar.server.debt.DebtModelXMLExporter.RuleDebt; + +import static com.google.common.collect.Lists.newArrayList; +import static java.lang.Boolean.FALSE; +import static java.lang.Boolean.TRUE; +import static javax.xml.stream.XMLInputFactory.IS_COALESCING; +import static javax.xml.stream.XMLInputFactory.IS_NAMESPACE_AWARE; +import static javax.xml.stream.XMLInputFactory.IS_VALIDATING; +import static javax.xml.stream.XMLInputFactory.SUPPORT_DTD; +import static org.apache.commons.lang.StringUtils.isNotBlank; +import static org.sonar.api.server.debt.DebtRemediationFunction.Type.CONSTANT_ISSUE; +import static org.sonar.api.server.debt.DebtRemediationFunction.Type.LINEAR; +import static org.sonar.api.utils.Duration.MINUTE; +import static org.sonar.server.debt.DebtModelXMLExporter.CHARACTERISTIC; +import static org.sonar.server.debt.DebtModelXMLExporter.PROPERTY; +import static org.sonar.server.debt.DebtModelXMLExporter.PROPERTY_COEFFICIENT; +import static org.sonar.server.debt.DebtModelXMLExporter.PROPERTY_FUNCTION; +import static org.sonar.server.debt.DebtModelXMLExporter.PROPERTY_KEY; +import static org.sonar.server.debt.DebtModelXMLExporter.PROPERTY_OFFSET; +import static org.sonar.server.debt.DebtModelXMLExporter.PROPERTY_TEXT_VALUE; +import static org.sonar.server.debt.DebtModelXMLExporter.PROPERTY_VALUE; +import static org.sonar.server.debt.DebtModelXMLExporter.REPOSITORY_KEY; +import static org.sonar.server.debt.DebtModelXMLExporter.RULE_KEY; + +/** + * Import rules debt definitions from an XML + */ +@ServerSide +public class DebtRulesXMLImporter { + + public List<RuleDebt> importXML(String xml, ValidationMessages validationMessages) { + return importXML(new StringReader(xml), validationMessages); + } + + public List<RuleDebt> importXML(Reader xml, ValidationMessages validationMessages) { + List<RuleDebt> ruleDebts = newArrayList(); + try { + SMInputFactory inputFactory = initStax(); + SMHierarchicCursor cursor = inputFactory.rootElementCursor(xml); + + // advance to <sqale> + cursor.advance(); + SMInputCursor rootCursor = cursor.childElementCursor(CHARACTERISTIC); + while (rootCursor.getNext() != null) { + process(ruleDebts, validationMessages, rootCursor); + } + + cursor.getStreamReader().closeCompletely(); + } catch (XMLStreamException e) { + throw new IllegalStateException("XML is not valid", e); + } + return ruleDebts; + } + + private static SMInputFactory initStax() { + XMLInputFactory xmlFactory = XMLInputFactory2.newInstance(); + xmlFactory.setProperty(IS_COALESCING, TRUE); + xmlFactory.setProperty(IS_NAMESPACE_AWARE, FALSE); + xmlFactory.setProperty(SUPPORT_DTD, FALSE); + xmlFactory.setProperty(IS_VALIDATING, FALSE); + return new SMInputFactory(xmlFactory); + } + + private static void process(List<RuleDebt> ruleDebts, + ValidationMessages validationMessages, SMInputCursor chcCursor) throws XMLStreamException { + SMInputCursor cursor = chcCursor.childElementCursor(); + while (cursor.getNext() != null) { + String node = cursor.getLocalName(); + if (StringUtils.equals(node, CHARACTERISTIC)) { + process(ruleDebts, validationMessages, cursor); + } else if (StringUtils.equals(node, REPOSITORY_KEY)) { + RuleDebt ruleDebt = processRule(validationMessages, cursor); + if (ruleDebt != null) { + ruleDebts.add(ruleDebt); + } + } + } + } + + @CheckForNull + private static RuleDebt processRule(ValidationMessages validationMessages, SMInputCursor cursor) throws XMLStreamException { + String ruleRepositoryKey = cursor.collectDescendantText().trim(); + String ruleKey = null; + Properties properties = new Properties(); + while (cursor.getNext() != null) { + String node = cursor.getLocalName(); + if (StringUtils.equals(node, PROPERTY)) { + properties.add(processProperty(validationMessages, cursor)); + } else if (StringUtils.equals(node, RULE_KEY)) { + ruleKey = cursor.collectDescendantText().trim(); + } + } + if (isNotBlank(ruleRepositoryKey) && isNotBlank(ruleKey)) { + return createRule(RuleKey.of(ruleRepositoryKey, ruleKey), properties, validationMessages); + } + return null; + } + + private static Property processProperty(ValidationMessages validationMessages, SMInputCursor cursor) throws XMLStreamException { + SMInputCursor c = cursor.childElementCursor(); + String key = null; + int value = 0; + String textValue = null; + while (c.getNext() != null) { + String node = c.getLocalName(); + if (StringUtils.equals(node, PROPERTY_KEY)) { + key = c.collectDescendantText().trim(); + + } else if (StringUtils.equals(node, PROPERTY_VALUE)) { + String s = c.collectDescendantText().trim(); + try { + Double valueDouble = NumberUtils.createDouble(s); + value = valueDouble.intValue(); + } catch (NumberFormatException ex) { + validationMessages.addErrorText(String.format("Cannot import value '%s' for field %s - Expected a numeric value instead", s, key)); + } + } else if (StringUtils.equals(node, PROPERTY_TEXT_VALUE)) { + textValue = c.collectDescendantText().trim(); + textValue = "mn".equals(textValue) ? MINUTE : textValue; + } + } + return new Property(key, value, textValue); + } + + @CheckForNull + private static RuleDebt createRule(RuleKey ruleKey, Properties properties, ValidationMessages validationMessages) { + Property function = properties.function(); + Property coefficientProperty = properties.coefficient(); + String coefficient = coefficientProperty == null ? null : coefficientProperty.toDuration(); + Property offsetProperty = properties.offset(); + String offset = offsetProperty == null ? null : offsetProperty.toDuration(); + if (function != null && (coefficient != null || offset != null)) { + return createRuleDebt(ruleKey, function.getTextValue(), coefficient, offset, validationMessages); + } + return null; + } + + @CheckForNull + private static RuleDebt createRuleDebt(RuleKey ruleKey, String function, @Nullable String coefficient, @Nullable String offset, ValidationMessages validationMessages) { + if ("constant_resource".equals(function)) { + validationMessages.addWarningText(String.format("Constant/file function is no longer used, technical debt definitions on '%s' are ignored.", ruleKey)); + return null; + } + if ("linear_threshold".equals(function) && coefficient != null) { + validationMessages.addWarningText(String.format("Linear with threshold function is no longer used, remediation function of '%s' is replaced by linear.", ruleKey)); + return createRuleDebt(ruleKey, LINEAR.name(), coefficient, null, validationMessages); + } + if (CONSTANT_ISSUE.name().equalsIgnoreCase(function) && coefficient != null && offset == null) { + return createRuleDebt(ruleKey, CONSTANT_ISSUE.name(), null, coefficient, validationMessages); + } + return new RuleDebt().setRuleKey(ruleKey).setFunction(function.toUpperCase()).setCoefficient(coefficient).setOffset(offset); + } + + private static class Properties { + List<Property> list; + + public Properties() { + this.list = newArrayList(); + } + + public Properties add(Property property) { + this.list.add(property); + return this; + } + + public Property function() { + return find(PROPERTY_FUNCTION); + } + + public Property coefficient() { + return find(PROPERTY_COEFFICIENT); + } + + public Property offset() { + return find(PROPERTY_OFFSET); + } + + private Property find(String key) { + return Iterables.find(list, new PropertyMatchKey(key), null); + } + } + + private static class Property { + String key; + int value; + String textValue; + + private Property(String key, int value, String textValue) { + this.key = key; + this.value = value; + this.textValue = textValue; + } + + private String getKey() { + return key; + } + + private int getValue() { + return value; + } + + private String getTextValue() { + return textValue; + } + + @CheckForNull + public String toDuration() { + if (key != null && getValue() > 0) { + String duration = Integer.toString(getValue()); + duration += !Strings.isNullOrEmpty(getTextValue()) ? getTextValue() : Duration.DAY; + return duration; + } + return null; + } + } + + private static class PropertyMatchKey implements Predicate<Property> { + private final String key; + + public PropertyMatchKey(String key) { + this.key = key; + } + + @Override + public boolean apply(@Nonnull Property input) { + return input.getKey().equals(key); + } + } + +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/debt/package-info.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/debt/package-info.java new file mode 100644 index 00000000000..6610578a903 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/debt/package-info.java @@ -0,0 +1,23 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +@ParametersAreNonnullByDefault +package org.sonar.server.debt; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/es/IndexerStartupTask.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/es/IndexerStartupTask.java new file mode 100644 index 00000000000..be64ce0a057 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/es/IndexerStartupTask.java @@ -0,0 +1,95 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.es; + +import java.util.Set; +import java.util.stream.Collectors; +import org.elasticsearch.action.admin.cluster.health.ClusterHealthAction; +import org.elasticsearch.client.Client; +import org.elasticsearch.common.unit.TimeValue; +import org.sonar.api.config.Configuration; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; +import org.sonar.api.utils.log.Profiler; +import org.sonar.server.es.metadata.MetadataIndex; + +import static java.util.Arrays.stream; +import static java.util.stream.Collectors.toSet; + +public class IndexerStartupTask { + + private static final Logger LOG = Loggers.get(IndexerStartupTask.class); + + private final EsClient esClient; + private final Configuration config; + private final MetadataIndex metadataIndex; + private final StartupIndexer[] indexers; + + public IndexerStartupTask(EsClient esClient, Configuration config, MetadataIndex metadataIndex, StartupIndexer... indexers) { + this.esClient = esClient; + this.config = config; + this.metadataIndex = metadataIndex; + this.indexers = indexers; + } + + public void execute() { + if (indexesAreEnabled()) { + stream(indexers) + .forEach(this::indexUninitializedTypes); + } + } + + private boolean indexesAreEnabled() { + return !config.getBoolean("sonar.internal.es.disableIndexes").orElse(false); + } + + private void indexUninitializedTypes(StartupIndexer indexer) { + Set<IndexType> uninitializedTypes = getUninitializedTypes(indexer); + if (!uninitializedTypes.isEmpty()) { + Profiler profiler = Profiler.create(LOG); + profiler.startInfo(getLogMessage(uninitializedTypes, "...")); + indexer.indexOnStartup(uninitializedTypes); + uninitializedTypes.forEach(this::setInitialized); + profiler.stopInfo(getLogMessage(uninitializedTypes, "done")); + } + } + + private Set<IndexType> getUninitializedTypes(StartupIndexer indexer) { + return indexer.getIndexTypes().stream() + .filter(indexType -> !metadataIndex.getInitialized(indexType)) + .collect(toSet()); + } + + private void setInitialized(IndexType indexType) { + waitForIndexYellow(indexType.getMainType().getIndex().getName()); + metadataIndex.setInitialized(indexType, true); + } + + private void waitForIndexYellow(String index) { + Client nativeClient = esClient.nativeClient(); + ClusterHealthAction.INSTANCE.newRequestBuilder(nativeClient).setIndices(index).setWaitForYellowStatus().get(TimeValue.timeValueMinutes(10)); + } + + private String getLogMessage(Set<IndexType> emptyTypes, String suffix) { + String s = emptyTypes.size() == 1 ? "" : "s"; + String typeList = emptyTypes.stream().map(Object::toString).collect(Collectors.joining(",")); + return String.format("Indexing of type%s %s %s", s, typeList, suffix); + } +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/es/MigrationEsClientImpl.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/es/MigrationEsClientImpl.java new file mode 100644 index 00000000000..d353a266466 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/es/MigrationEsClientImpl.java @@ -0,0 +1,77 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.es; + +import com.google.common.collect.Maps; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.elasticsearch.action.admin.indices.stats.IndexStats; +import org.sonar.api.utils.log.Loggers; +import org.sonar.core.util.stream.MoreCollectors; +import org.sonar.server.platform.db.migration.es.MigrationEsClient; + +public class MigrationEsClientImpl implements MigrationEsClient { + private final EsClient client; + private final Set<String> updatedIndices = new HashSet<>(); + + public MigrationEsClientImpl(EsClient client) { + this.client = client; + } + + @Override + public void deleteIndexes(String name, String... otherNames) { + Map<String, IndexStats> indices = client.nativeClient().admin().indices().prepareStats().get().getIndices(); + Set<String> existingIndices = indices.values().stream().map(IndexStats::getIndex).collect(MoreCollectors.toSet()); + Stream.concat(Stream.of(name), Arrays.stream(otherNames)) + .distinct() + .filter(existingIndices::contains) + .forEach(this::deleteIndex); + } + + @Override + public void addMappingToExistingIndex(String index, String type, String mappingName, String mappingType, Map<String, String> options) { + IndexStats stats = client.nativeClient().admin().indices().prepareStats().get().getIndex(index); + if (stats != null) { + Loggers.get(getClass()).info("Add mapping [{}] to Elasticsearch index [{}]", mappingName, index); + String mappingOptions = Stream.concat(Stream.of(Maps.immutableEntry("type", mappingType)), options.entrySet().stream()) + .map(e -> e.getKey() + "=" + e.getValue()) + .collect(Collectors.joining(",")); + client.nativeClient().admin().indices().preparePutMapping(index) + .setType(type) + .setSource(mappingName, mappingOptions) + .get(); + updatedIndices.add(index); + } + } + + @Override + public Set<String> getUpdatedIndices() { + return updatedIndices; + } + + private void deleteIndex(String index) { + Loggers.get(getClass()).info("Drop Elasticsearch index [{}]", index); + client.nativeClient().admin().indices().prepareDelete(index).get(); + } +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/es/package-info.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/es/package-info.java new file mode 100644 index 00000000000..df6be07802c --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/es/package-info.java @@ -0,0 +1,24 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +@ParametersAreNonnullByDefault +package org.sonar.server.es; + +import javax.annotation.ParametersAreNonnullByDefault; + diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/notification/NotificationDaemon.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/notification/NotificationDaemon.java new file mode 100644 index 00000000000..acf90de9586 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/notification/NotificationDaemon.java @@ -0,0 +1,138 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.notification; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import org.picocontainer.Startable; +import org.sonar.api.Properties; +import org.sonar.api.Property; +import org.sonar.api.config.Configuration; +import org.sonar.api.notifications.Notification; +import org.sonar.api.server.ServerSide; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; + +import static java.util.Collections.singleton; + +@Properties({ + @Property( + key = NotificationDaemon.PROPERTY_DELAY, + defaultValue = "60", + name = "Delay of notifications, in seconds", + global = false), + @Property( + key = NotificationDaemon.PROPERTY_DELAY_BEFORE_REPORTING_STATUS, + defaultValue = "600", + name = "Delay before reporting notification status, in seconds", + global = false) +}) +@ServerSide +public class NotificationDaemon implements Startable { + private static final String THREAD_NAME_PREFIX = "sq-notification-service-"; + + private static final Logger LOG = Loggers.get(NotificationDaemon.class); + + public static final String PROPERTY_DELAY = "sonar.notifications.delay"; + public static final String PROPERTY_DELAY_BEFORE_REPORTING_STATUS = "sonar.notifications.runningDelayBeforeReportingStatus"; + + private final long delayInSeconds; + private final long delayBeforeReportingStatusInSeconds; + private final DefaultNotificationManager manager; + private final NotificationService service; + + private ScheduledExecutorService executorService; + private boolean stopping = false; + + public NotificationDaemon(Configuration config, DefaultNotificationManager manager, NotificationService service) { + this.delayInSeconds = config.getLong(PROPERTY_DELAY).get(); + this.delayBeforeReportingStatusInSeconds = config.getLong(PROPERTY_DELAY_BEFORE_REPORTING_STATUS).get(); + this.manager = manager; + this.service = service; + } + + @Override + public void start() { + executorService = Executors.newSingleThreadScheduledExecutor( + new ThreadFactoryBuilder() + .setNameFormat(THREAD_NAME_PREFIX + "%d") + .setPriority(Thread.MIN_PRIORITY) + .build()); + executorService.scheduleWithFixedDelay(() -> { + try { + processQueue(); + } catch (Exception e) { + LOG.error("Error in NotificationService", e); + } + }, 0, delayInSeconds, TimeUnit.SECONDS); + LOG.info("Notification service started (delay {} sec.)", delayInSeconds); + } + + @Override + public void stop() { + try { + stopping = true; + executorService.shutdown(); + executorService.awaitTermination(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + LOG.error("Error during stop of notification service", e); + Thread.currentThread().interrupt(); + } + LOG.info("Notification service stopped"); + } + + private synchronized void processQueue() { + long start = now(); + long lastLog = start; + long notifSentCount = 0; + + Notification notifToSend = manager.getFromQueue(); + while (notifToSend != null) { + notifSentCount += service.deliverEmails(singleton(notifToSend)); + // compatibility with old API + notifSentCount += service.deliver(notifToSend); + if (stopping) { + break; + } + long now = now(); + if (now - lastLog > delayBeforeReportingStatusInSeconds * 1000) { + long remainingNotifCount = manager.count(); + lastLog = now; + long spentTimeInMinutes = (now - start) / (60 * 1000); + log(notifSentCount, remainingNotifCount, spentTimeInMinutes); + } + notifToSend = manager.getFromQueue(); + } + } + + @VisibleForTesting + void log(long notifSentCount, long remainingNotifCount, long spentTimeInMinutes) { + LOG.info("{} notifications sent during the past {} minutes and {} still waiting to be sent", + notifSentCount, spentTimeInMinutes, remainingNotifCount); + } + + @VisibleForTesting + long now() { + return System.currentTimeMillis(); + } +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/notification/NotificationModule.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/notification/NotificationModule.java new file mode 100644 index 00000000000..abe04c57386 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/notification/NotificationModule.java @@ -0,0 +1,36 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.notification; + +import org.sonar.api.config.EmailSettings; +import org.sonar.core.platform.Module; +import org.sonar.server.notification.email.EmailNotificationChannel; + +public class NotificationModule extends Module { + @Override + protected void configureModule() { + add( + EmailSettings.class, + NotificationService.class, + DefaultNotificationManager.class, + NotificationDaemon.class, + EmailNotificationChannel.class); + } +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/notification/package-info.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/notification/package-info.java new file mode 100644 index 00000000000..a38c847a25f --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/notification/package-info.java @@ -0,0 +1,23 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +@ParametersAreNonnullByDefault +package org.sonar.server.notification; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/AbstractSystemInfoWriter.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/AbstractSystemInfoWriter.java new file mode 100644 index 00000000000..fad900559ed --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/AbstractSystemInfoWriter.java @@ -0,0 +1,93 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform; + +import java.util.Collection; +import org.sonar.api.utils.text.JsonWriter; +import org.sonar.process.systeminfo.SystemInfoUtils; +import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo; +import org.sonar.server.health.Health; +import org.sonar.server.telemetry.TelemetryDataLoader; + +import static org.sonar.server.telemetry.TelemetryDataJsonWriter.writeTelemetryData; + +public abstract class AbstractSystemInfoWriter implements SystemInfoWriter { + private static final String[] ORDERED_SECTION_NAMES = { + // standalone + "System", "Database", "Plugins", + + // cluster + "Web JVM State", "Web Database Connection", "Web Logging", "Web JVM Properties", + "Compute Engine Tasks", "Compute Engine JVM State", "Compute Engine Database Connection", "Compute Engine Logging", "Compute Engine JVM Properties", + "Search State", "Search Indexes"}; + + private final TelemetryDataLoader telemetry; + + AbstractSystemInfoWriter(TelemetryDataLoader telemetry) { + this.telemetry = telemetry; + } + + protected void writeSections(Collection<ProtobufSystemInfo.Section> sections, JsonWriter json) { + SystemInfoUtils + .order(sections, ORDERED_SECTION_NAMES) + .forEach(section -> writeSection(section, json)); + } + + private void writeSection(ProtobufSystemInfo.Section section, JsonWriter json) { + json.name(section.getName()); + json.beginObject(); + for (ProtobufSystemInfo.Attribute attribute : section.getAttributesList()) { + writeAttribute(attribute, json); + } + json.endObject(); + } + + private void writeAttribute(ProtobufSystemInfo.Attribute attribute, JsonWriter json) { + switch (attribute.getValueCase()) { + case BOOLEAN_VALUE: + json.prop(attribute.getKey(), attribute.getBooleanValue()); + break; + case LONG_VALUE: + json.prop(attribute.getKey(), attribute.getLongValue()); + break; + case DOUBLE_VALUE: + json.prop(attribute.getKey(), attribute.getDoubleValue()); + break; + case STRING_VALUE: + json.prop(attribute.getKey(), attribute.getStringValue()); + break; + case VALUE_NOT_SET: + json.name(attribute.getKey()).beginArray().values(attribute.getStringValuesList()).endArray(); + break; + default: + throw new IllegalArgumentException("Unsupported type: " + attribute.getValueCase()); + } + } + + protected void writeHealth(Health health, JsonWriter json) { + json.prop("Health", health.getStatus().name()); + json.name("Health Causes").beginArray().values(health.getCauses()).endArray(); + } + + protected void writeTelemetry(JsonWriter json) { + json.name("Statistics"); + writeTelemetryData(json, telemetry.load()); + } +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/BackendCleanup.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/BackendCleanup.java new file mode 100644 index 00000000000..e4fc7c2bce6 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/BackendCleanup.java @@ -0,0 +1,249 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform; + +import com.google.common.collect.ImmutableMap; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import org.sonar.api.server.ServerSide; +import org.sonar.api.utils.log.Loggers; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.dialect.Oracle; +import org.sonar.db.version.SqTables; +import org.sonar.server.component.index.ComponentIndexDefinition; +import org.sonar.server.es.BulkIndexer; +import org.sonar.server.es.EsClient; +import org.sonar.server.es.Index; +import org.sonar.server.es.IndexType; +import org.sonar.server.issue.index.IssueIndexDefinition; +import org.sonar.server.measure.index.ProjectMeasuresIndexDefinition; +import org.sonar.server.property.InternalProperties; +import org.sonar.server.view.index.ViewIndexDefinition; + +import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; + +@ServerSide +public class BackendCleanup { + + private static final String[] ANALYSIS_TABLES = { + "ce_activity", "ce_queue", "ce_task_input", "ce_scanner_context", + "duplications_index", "events", "issues", "issue_changes", "manual_measures", + "notifications", "project_links", "project_measures", "projects", + "snapshots", "file_sources", "webhook_deliveries" + }; + private static final String[] RESOURCE_RELATED_TABLES = { + "group_roles", "user_roles", "properties" + }; + private static final Map<String, TableCleaner> TABLE_CLEANERS = ImmutableMap.of( + "organizations", BackendCleanup::truncateOrganizations, + "users", BackendCleanup::truncateUsers, + "groups", BackendCleanup::truncateGroups, + "internal_properties", BackendCleanup::truncateInternalProperties, + "schema_migrations", BackendCleanup::truncateSchemaMigrations); + + private final EsClient esClient; + private final DbClient dbClient; + + public BackendCleanup(EsClient esClient, DbClient dbClient) { + this.esClient = esClient; + this.dbClient = dbClient; + } + + public void clearAll() { + clearDb(); + clearIndexes(); + } + + public void clearDb() { + try (DbSession dbSession = dbClient.openSession(false); + Connection connection = dbSession.getConnection(); + Statement ddlStatement = connection.createStatement()) { + for (String tableName : SqTables.TABLES) { + Optional.ofNullable(TABLE_CLEANERS.get(tableName)) + .orElse(BackendCleanup::truncateDefault) + .clean(tableName, ddlStatement, connection); + } + } catch (Exception e) { + throw new IllegalStateException("Fail to clear db", e); + } + } + + public void clearIndexes() { + Loggers.get(getClass()).info("Truncate Elasticsearch indices"); + try { + esClient.prepareClearCache().get(); + + for (String index : esClient.prepareState().get().getState().getMetaData().getConcreteAllIndices()) { + /*under the hood, type is not used to perform clearIndex, so it's ok it does not match any existing index*/ + clearIndex(Index.simple(index)); + } + } catch (Exception e) { + throw new IllegalStateException("Unable to clear indexes", e); + } + } + + /** + * Reset data in order to to be in same state as a fresh installation (but without having to drop db and restart the server). + * + * Please be careful when updating this method as it's called by Orchestrator. + */ + public void resetData() { + try (DbSession dbSession = dbClient.openSession(false); + Connection connection = dbSession.getConnection()) { + + truncateAnalysisTables(connection); + deleteManualRules(connection); + truncateInternalProperties(null, null, connection); + truncateUsers(null, null, connection); + truncateOrganizations(null, null, connection); + } catch (SQLException e) { + throw new IllegalStateException("Fail to reset data", e); + } + + clearIndex(IssueIndexDefinition.DESCRIPTOR); + clearIndex(ViewIndexDefinition.DESCRIPTOR); + clearIndex(ProjectMeasuresIndexDefinition.DESCRIPTOR); + clearIndex(ComponentIndexDefinition.DESCRIPTOR); + } + + private void truncateAnalysisTables(Connection connection) throws SQLException { + try (Statement statement = connection.createStatement()) { + // Clear inspection tables + for (String table : ANALYSIS_TABLES) { + statement.execute(createTruncateSql(table.toLowerCase(Locale.ENGLISH))); + // commit is useless on some databases + connection.commit(); + } + // Clear resource related tables + for (String table : RESOURCE_RELATED_TABLES) { + statement.execute("DELETE FROM " + table + " WHERE resource_id IS NOT NULL"); + connection.commit(); + } + } + } + + private String createTruncateSql(String table) { + if (dbClient.getDatabase().getDialect().getId().equals(Oracle.ID)) { + // truncate operation is needs to lock the table on Oracle. Unfortunately + // it fails sometimes in our QA environment because table is locked. + // We never found the root cause (no locks found when displaying them just after + // receiving the error). + // Workaround is to use "delete" operation. It does not require lock on table. + return "DELETE FROM " + table; + } + return "TRUNCATE TABLE " + table; + } + + private static void deleteManualRules(Connection connection) throws SQLException { + try (PreparedStatement statement = connection.prepareStatement("DELETE FROM rules WHERE rules.plugin_name='manual'")) { + statement.execute(); + // commit is useless on some databases + connection.commit(); + } + } + + /** + * Completely remove a index with all types + */ + public void clearIndex(Index index) { + BulkIndexer.delete(esClient, IndexType.main(index, index.getName()), esClient.prepareSearch(index).setQuery(matchAllQuery())); + } + + @FunctionalInterface + private interface TableCleaner { + void clean(String tableName, Statement ddlStatement, Connection connection) throws SQLException; + } + + private static void truncateDefault(String tableName, Statement ddlStatement, Connection connection) throws SQLException { + ddlStatement.execute("TRUNCATE TABLE " + tableName.toLowerCase(Locale.ENGLISH)); + // commit is useless on some databases + connection.commit(); + } + + /** + * Default organization must never be deleted + */ + private static void truncateOrganizations(String tableName, Statement ddlStatement, Connection connection) throws SQLException { + try (PreparedStatement preparedStatement = connection.prepareStatement("delete from organizations where kee <> ?")) { + preparedStatement.setString(1, "default-organization"); + preparedStatement.execute(); + // commit is useless on some databases + connection.commit(); + } + } + + /** + * User admin must never be deleted. + */ + private static void truncateUsers(String tableName, Statement ddlStatement, Connection connection) throws SQLException { + try (PreparedStatement preparedStatement = connection.prepareStatement("delete from users where login <> ?")) { + preparedStatement.setString(1, "admin"); + preparedStatement.execute(); + // commit is useless on some databases + connection.commit(); + } + // "admin" is not flagged as root by default + try (PreparedStatement preparedStatement = connection.prepareStatement("update users set is_root=?")) { + preparedStatement.setBoolean(1, false); + preparedStatement.execute(); + // commit is useless on some databases + connection.commit(); + } + } + + /** + * Groups sonar-users is referenced by the default organization as its default group. + */ + private static void truncateGroups(String tableName, Statement ddlStatement, Connection connection) throws SQLException { + try (PreparedStatement preparedStatement = connection.prepareStatement("delete from groups where name <> ?")) { + preparedStatement.setString(1, "sonar-users"); + preparedStatement.execute(); + // commit is useless on some databases + connection.commit(); + } + } + + /** + * Internal property {@link InternalProperties#DEFAULT_ORGANIZATION} must never be deleted. + */ + private static void truncateInternalProperties(String tableName, Statement ddlStatement, Connection connection) throws SQLException { + try (PreparedStatement preparedStatement = connection.prepareStatement("delete from internal_properties where kee not in (?,?)")) { + preparedStatement.setString(1, InternalProperties.DEFAULT_ORGANIZATION); + preparedStatement.setString(2, InternalProperties.SERVER_ID_CHECKSUM); + preparedStatement.execute(); + // commit is useless on some databases + connection.commit(); + } + } + + /** + * Data in SCHEMA_MIGRATIONS table is inserted when DB is created and should not be altered afterwards. + */ + private static void truncateSchemaMigrations(String tableName, Statement ddlStatement, Connection connection) { + // do nothing + } + +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/ClusterVerification.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/ClusterVerification.java new file mode 100644 index 00000000000..f8659270705 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/ClusterVerification.java @@ -0,0 +1,58 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform; + +import javax.annotation.Nullable; +import org.sonar.api.Startable; +import org.sonar.api.server.ServerSide; +import org.sonar.api.utils.MessageException; + +@ServerSide +public class ClusterVerification implements Startable { + + private final WebServer server; + @Nullable + private final ClusterFeature feature; + + public ClusterVerification(WebServer server, @Nullable ClusterFeature feature) { + this.server = server; + this.feature = feature; + } + + public ClusterVerification(WebServer server) { + this(server, null); + } + + @Override + public void start() { + if (server.isStandalone()) { + return; + } + if (feature == null || !feature.isEnabled()) { + throw MessageException.of( + "Cluster mode can't be enabled. Please install the Data Center Edition. More details at https://redirect.sonarsource.com/editions/datacenter.html."); + } + } + + @Override + public void stop() { + // nothing to do + } +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/DatabaseServerCompatibility.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/DatabaseServerCompatibility.java new file mode 100644 index 00000000000..ea8a7f7e336 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/DatabaseServerCompatibility.java @@ -0,0 +1,73 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform; + +import java.util.Optional; +import org.picocontainer.Startable; +import org.sonar.api.config.Configuration; +import org.sonar.api.utils.MessageException; +import org.sonar.api.utils.log.Loggers; +import org.sonar.process.ProcessProperties; +import org.sonar.server.platform.db.migration.version.DatabaseVersion; + +import static org.sonar.server.log.ServerProcessLogging.STARTUP_LOGGER_NAME; + +public class DatabaseServerCompatibility implements Startable { + + private static final String HIGHLIGHTER = "################################################################################"; + + private final DatabaseVersion version; + private final Configuration configuration; + + public DatabaseServerCompatibility(DatabaseVersion version, Configuration configuration) { + this.version = version; + this.configuration = configuration; + } + + @Override + public void start() { + DatabaseVersion.Status status = version.getStatus(); + if (status == DatabaseVersion.Status.REQUIRES_DOWNGRADE) { + throw MessageException.of("Database was upgraded to a more recent version of SonarQube. " + + "A backup must probably be restored or the DB settings are incorrect."); + } + if (status == DatabaseVersion.Status.REQUIRES_UPGRADE) { + Optional<Long> currentVersion = this.version.getVersion(); + if (currentVersion.isPresent() && currentVersion.get() < DatabaseVersion.MIN_UPGRADE_VERSION) { + throw MessageException.of("Current version is too old. Please upgrade to Long Term Support version firstly."); + } + boolean blueGreen = configuration.getBoolean(ProcessProperties.Property.BLUE_GREEN_ENABLED.getKey()).orElse(false); + if (!blueGreen) { + String msg = "The database must be manually upgraded. Please backup the database and browse /setup. " + + "For more information: https://docs.sonarqube.org/latest/setup/upgrading"; + Loggers.get(DatabaseServerCompatibility.class).warn(msg); + Loggers.get(STARTUP_LOGGER_NAME).warn('\n' + + HIGHLIGHTER + '\n' + + " " + msg + + '\n' + HIGHLIGHTER); + } + } + } + + @Override + public void stop() { + // do nothing + } +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/DefaultServerUpgradeStatus.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/DefaultServerUpgradeStatus.java new file mode 100644 index 00000000000..6948c10225b --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/DefaultServerUpgradeStatus.java @@ -0,0 +1,81 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform; + +import java.util.Optional; +import org.apache.commons.lang.builder.ReflectionToStringBuilder; +import org.apache.commons.lang.builder.ToStringStyle; +import org.picocontainer.Startable; +import org.sonar.api.config.Configuration; +import org.sonar.api.platform.ServerUpgradeStatus; +import org.sonar.process.ProcessProperties; +import org.sonar.server.platform.db.migration.step.MigrationSteps; +import org.sonar.server.platform.db.migration.version.DatabaseVersion; + +public class DefaultServerUpgradeStatus implements ServerUpgradeStatus, Startable { + + private final DatabaseVersion dbVersion; + private final MigrationSteps migrationSteps; + private final Configuration configuration; + + // available when connected to db + private long initialDbVersion; + + public DefaultServerUpgradeStatus(DatabaseVersion dbVersion, MigrationSteps migrationSteps, Configuration configuration) { + this.dbVersion = dbVersion; + this.migrationSteps = migrationSteps; + this.configuration = configuration; + } + + @Override + public void start() { + Optional<Long> v = dbVersion.getVersion(); + this.initialDbVersion = v.orElse(-1L); + } + + @Override + public void stop() { + // do nothing + } + + @Override + public boolean isUpgraded() { + return !isFreshInstall() && (initialDbVersion < migrationSteps.getMaxMigrationNumber()); + } + + @Override + public boolean isFreshInstall() { + return initialDbVersion < 0; + } + + @Override + public int getInitialDbVersion() { + return (int) initialDbVersion; + } + + public boolean isBlueGreen() { + return configuration.getBoolean(ProcessProperties.Property.BLUE_GREEN_ENABLED.getKey()).orElse(false); + } + + @Override + public String toString() { + return new ReflectionToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE).toString(); + } +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/LogServerVersion.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/LogServerVersion.java new file mode 100644 index 00000000000..ed78bd44f2b --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/LogServerVersion.java @@ -0,0 +1,64 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform; + +import com.google.common.base.Joiner; +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; +import org.sonar.api.SonarRuntime; +import org.sonar.api.Startable; +import org.sonar.api.server.ServerSide; +import org.sonar.api.utils.Version; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; + +@ServerSide +public class LogServerVersion implements Startable { + + private static final Logger LOG = Loggers.get(LogServerVersion.class); + private final SonarRuntime runtime; + + public LogServerVersion(SonarRuntime runtime) { + this.runtime = runtime; + } + + @Override + public void start() { + String scmRevision = read("/build.properties").getProperty("Implementation-Build"); + Version version = runtime.getApiVersion(); + LOG.info("SonarQube {}", Joiner.on(" / ").skipNulls().join("Server", version, scmRevision)); + } + + @Override + public void stop() { + // nothing to do + } + + private static Properties read(String filePath) { + try (InputStream stream = LogServerVersion.class.getResourceAsStream(filePath)) { + Properties properties = new Properties(); + properties.load(stream); + return properties; + } catch (IOException e) { + throw new IllegalStateException("Fail to read file " + filePath + " from classpath", e); + } + } +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/PersistentSettings.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/PersistentSettings.java new file mode 100644 index 00000000000..818e13930c3 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/PersistentSettings.java @@ -0,0 +1,84 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform; + +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; +import org.sonar.api.config.Settings; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.property.PropertyDto; +import org.sonar.server.setting.SettingsChangeNotifier; + +public class PersistentSettings { + + private final Settings delegate; + private final DbClient dbClient; + private final SettingsChangeNotifier changeNotifier; + + public PersistentSettings(Settings delegate, DbClient dbClient, SettingsChangeNotifier changeNotifier) { + this.delegate = delegate; + this.dbClient = dbClient; + this.changeNotifier = changeNotifier; + } + + @CheckForNull + public String getString(String key) { + return delegate.getString(key); + } + + /** + * Insert property into database if value is not {@code null}, else delete property from + * database. Session is not committed but {@link org.sonar.api.config.GlobalPropertyChangeHandler} + * are executed. + */ + public PersistentSettings saveProperty(DbSession dbSession, String key, @Nullable String value) { + savePropertyImpl(dbSession, key, value); + changeNotifier.onGlobalPropertyChange(key, value); + return this; + } + + /** + * Same as {@link #saveProperty(DbSession, String, String)} but a new database session is + * opened and committed. + */ + public PersistentSettings saveProperty(String key, @Nullable String value) { + try (DbSession dbSession = dbClient.openSession(false)) { + savePropertyImpl(dbSession, key, value); + dbSession.commit(); + changeNotifier.onGlobalPropertyChange(key, value); + return this; + } + } + + private void savePropertyImpl(DbSession dbSession, String key, @Nullable String value) { + if (value == null) { + dbClient.propertiesDao().deleteGlobalProperty(key, dbSession); + } else { + dbClient.propertiesDao().saveProperty(dbSession, new PropertyDto().setKey(key).setValue(value)); + } + // refresh the cache of settings + delegate.setProperty(key, value); + } + + public Settings getSettings() { + return delegate; + } +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/StartupMetadataPersister.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/StartupMetadataPersister.java new file mode 100644 index 00000000000..41fa223b2f2 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/StartupMetadataPersister.java @@ -0,0 +1,64 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform; + +import java.util.Date; +import org.sonar.api.CoreProperties; +import org.sonar.api.Startable; +import org.sonar.api.server.ServerSide; +import org.sonar.api.utils.DateUtils; +import org.sonar.db.DbClient; +import org.sonar.db.property.PropertyDto; + +/** + * The server node marked as "startup leader" generates some information about startup. These + * information are loaded by "startup follower" servers and all Compute Engine nodes. + * + * @see StartupMetadataProvider#load(DbClient) + */ +@ServerSide +public class StartupMetadataPersister implements Startable { + + private final StartupMetadata metadata; + // PersistentSettings can not be used as it has to be + // instantiated in level 4 of container, whereas + // StartupMetadataPersister is level 3. + private final DbClient dbClient; + + public StartupMetadataPersister(StartupMetadata metadata, DbClient dbClient) { + this.metadata = metadata; + this.dbClient = dbClient; + } + + @Override + public void start() { + String startedAt = DateUtils.formatDateTime(new Date(metadata.getStartedAt())); + save(CoreProperties.SERVER_STARTTIME, startedAt); + } + + private void save(String key, String value) { + dbClient.propertiesDao().saveProperty(new PropertyDto().setKey(key).setValue(value)); + } + + @Override + public void stop() { + // nothing to do + } +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/WebCoreExtensionsInstaller.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/WebCoreExtensionsInstaller.java new file mode 100644 index 00000000000..26f2df7e52f --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/WebCoreExtensionsInstaller.java @@ -0,0 +1,32 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform; + +import org.sonar.api.SonarRuntime; +import org.sonar.api.server.ServerSide; +import org.sonar.core.extension.CoreExtensionRepository; +import org.sonar.core.extension.CoreExtensionsInstaller; + +@ServerSide +public class WebCoreExtensionsInstaller extends CoreExtensionsInstaller { + public WebCoreExtensionsInstaller(SonarRuntime sonarRuntime, CoreExtensionRepository coreExtensionRepository) { + super(sonarRuntime, coreExtensionRepository, ServerSide.class); + } +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/CheckDatabaseCharsetAtStartup.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/CheckDatabaseCharsetAtStartup.java new file mode 100644 index 00000000000..345e3dbc1c3 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/CheckDatabaseCharsetAtStartup.java @@ -0,0 +1,56 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.db; + +import org.picocontainer.Startable; +import org.sonar.api.platform.ServerUpgradeStatus; +import org.sonar.server.platform.db.migration.charset.DatabaseCharsetChecker; + +/** + * Checks charset of all existing database columns at startup, before executing db migrations. This requires + * to be defined in platform level 2 ({@link org.sonar.server.platform.platformlevel.PlatformLevel2}). + */ +public class CheckDatabaseCharsetAtStartup implements Startable { + + private final ServerUpgradeStatus upgradeStatus; + private final DatabaseCharsetChecker charsetChecker; + + public CheckDatabaseCharsetAtStartup(ServerUpgradeStatus upgradeStatus, DatabaseCharsetChecker charsetChecker) { + this.upgradeStatus = upgradeStatus; + this.charsetChecker = charsetChecker; + } + + @Override + public void start() { + DatabaseCharsetChecker.State state = DatabaseCharsetChecker.State.STARTUP; + if (upgradeStatus.isUpgraded()) { + state = DatabaseCharsetChecker.State.UPGRADE; + } else if (upgradeStatus.isFreshInstall()) { + state = DatabaseCharsetChecker.State.FRESH_INSTALL; + } + charsetChecker.check(state); + } + + @Override + public void stop() { + // do nothing + } + +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/EmbeddedDatabase.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/EmbeddedDatabase.java new file mode 100644 index 00000000000..6cb54bb6e51 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/EmbeddedDatabase.java @@ -0,0 +1,116 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.db; + +import java.io.File; +import java.net.InetAddress; +import java.sql.DriverManager; +import java.sql.SQLException; +import org.h2.Driver; +import org.h2.tools.Server; +import org.picocontainer.Startable; +import org.sonar.api.config.Configuration; +import org.sonar.api.utils.System2; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; + +import static com.google.common.base.Preconditions.checkArgument; +import static java.lang.String.format; +import static org.apache.commons.lang.StringUtils.isNotEmpty; +import static org.sonar.process.ProcessProperties.Property.JDBC_EMBEDDED_PORT; +import static org.sonar.process.ProcessProperties.Property.JDBC_PASSWORD; +import static org.sonar.process.ProcessProperties.Property.JDBC_URL; +import static org.sonar.process.ProcessProperties.Property.JDBC_USERNAME; +import static org.sonar.process.ProcessProperties.Property.PATH_DATA; + +public class EmbeddedDatabase implements Startable { + private static final Logger LOG = Loggers.get(EmbeddedDatabase.class); + + private final Configuration config; + private final System2 system2; + private Server server; + + public EmbeddedDatabase(Configuration config, System2 system2) { + this.config = config; + this.system2 = system2; + } + + @Override + public void start() { + File dbHome = new File(getRequiredSetting(PATH_DATA.getKey())); + if (!dbHome.exists()) { + dbHome.mkdirs(); + } + + startServer(dbHome); + } + + private void startServer(File dbHome) { + String url = getRequiredSetting(JDBC_URL.getKey()); + String port = getRequiredSetting(JDBC_EMBEDDED_PORT.getKey()); + String user = getSetting(JDBC_USERNAME.getKey()); + String password = getSetting(JDBC_PASSWORD.getKey()); + try { + // Db is used only by web server and compute engine. No need + // to make it accessible from outside. + system2.setProperty("h2.bindAddress", InetAddress.getLoopbackAddress().getHostAddress()); + + if (url.contains("/mem:")) { + server = Server.createTcpServer("-tcpPort", port, "-baseDir", dbHome.getAbsolutePath()); + } else { + createDatabase(dbHome, user, password); + server = Server.createTcpServer("-tcpPort", port, "-ifExists", "-baseDir", dbHome.getAbsolutePath()); + } + + LOG.info("Starting embedded database on port " + server.getPort() + " with url " + url); + server.start(); + + LOG.info("Embedded database started. Data stored in: " + dbHome.getAbsolutePath()); + } catch (SQLException e) { + throw new IllegalStateException("Unable to start database", e); + } + } + + @Override + public void stop() { + if (server != null) { + server.stop(); + server = null; + LOG.info("Embedded database stopped"); + } + } + + private String getRequiredSetting(String property) { + String value = config.get(property).orElse(""); + checkArgument(isNotEmpty(value), "Missing property %s", property); + return value; + } + + private String getSetting(String name) { + return config.get(name).orElse(""); + } + + private static void createDatabase(File dbHome, String user, String password) throws SQLException { + String url = format("jdbc:h2:%s/sonar;USER=%s;PASSWORD=%s", dbHome.getAbsolutePath(), user, password); + + DriverManager.registerDriver(new Driver()); + DriverManager.getConnection(url).close(); + } +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/EmbeddedDatabaseFactory.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/EmbeddedDatabaseFactory.java new file mode 100644 index 00000000000..9ed180873d3 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/EmbeddedDatabaseFactory.java @@ -0,0 +1,66 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.db; + +import com.google.common.annotations.VisibleForTesting; +import org.picocontainer.Startable; +import org.sonar.api.config.Configuration; +import org.sonar.api.utils.System2; + +import static org.apache.commons.lang.StringUtils.startsWith; +import static org.sonar.process.ProcessProperties.Property.JDBC_URL; + +public class EmbeddedDatabaseFactory implements Startable { + + private static final String URL_PREFIX = "jdbc:h2:tcp:"; + + private final Configuration config; + private final System2 system2; + private EmbeddedDatabase embeddedDatabase; + + public EmbeddedDatabaseFactory(Configuration config, System2 system2) { + this.config = config; + this.system2 = system2; + } + + @Override + public void start() { + if (embeddedDatabase == null) { + String jdbcUrl = config.get(JDBC_URL.getKey()).get(); + if (startsWith(jdbcUrl, URL_PREFIX)) { + embeddedDatabase = createEmbeddedDatabase(); + embeddedDatabase.start(); + } + } + } + + @Override + public void stop() { + if (embeddedDatabase != null) { + embeddedDatabase.stop(); + embeddedDatabase = null; + } + } + + @VisibleForTesting + EmbeddedDatabase createEmbeddedDatabase() { + return new EmbeddedDatabase(config, system2); + } +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/migration/AutoDbMigration.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/migration/AutoDbMigration.java new file mode 100644 index 00000000000..7327cacb0f2 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/migration/AutoDbMigration.java @@ -0,0 +1,52 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.db.migration; + +import org.picocontainer.Startable; +import org.sonar.api.utils.log.Loggers; +import org.sonar.server.platform.DefaultServerUpgradeStatus; +import org.sonar.server.platform.db.migration.engine.MigrationEngine; + +public class AutoDbMigration implements Startable { + private final DefaultServerUpgradeStatus serverUpgradeStatus; + private final MigrationEngine migrationEngine; + + public AutoDbMigration(DefaultServerUpgradeStatus serverUpgradeStatus, MigrationEngine migrationEngine) { + this.serverUpgradeStatus = serverUpgradeStatus; + this.migrationEngine = migrationEngine; + } + + @Override + public void start() { + if (serverUpgradeStatus.isFreshInstall()) { + Loggers.get(getClass()).info("Automatically perform DB migration on fresh install"); + migrationEngine.execute(); + } else if (serverUpgradeStatus.isUpgraded() && serverUpgradeStatus.isBlueGreen()) { + Loggers.get(getClass()).info("Automatically perform DB migration on blue/green deployment"); + migrationEngine.execute(); + } + } + + @Override + public void stop() { + // nothing to do + } + +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/migration/DatabaseMigrationExecutorService.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/migration/DatabaseMigrationExecutorService.java new file mode 100644 index 00000000000..63306e38ac8 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/migration/DatabaseMigrationExecutorService.java @@ -0,0 +1,29 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.db.migration; + +import java.util.concurrent.ExecutorService; + +/** + * Flag interface for the ExecutorService to be used by the {@link DatabaseMigrationImpl} + * component. + */ +public interface DatabaseMigrationExecutorService extends ExecutorService { +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/migration/DatabaseMigrationExecutorServiceImpl.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/migration/DatabaseMigrationExecutorServiceImpl.java new file mode 100644 index 00000000000..659b8fbcd10 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/migration/DatabaseMigrationExecutorServiceImpl.java @@ -0,0 +1,44 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.db.migration; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import org.sonar.server.util.AbstractStoppableExecutorService; + +import java.util.concurrent.Executors; + +/** + * Since only one DB migration can run at a time, this implementation of DatabaseMigrationExecutorService + * wraps a single thread executor from the JDK. + */ +public class DatabaseMigrationExecutorServiceImpl + extends AbstractStoppableExecutorService + implements DatabaseMigrationExecutorService { + + public DatabaseMigrationExecutorServiceImpl() { + super( + Executors.newSingleThreadExecutor( + new ThreadFactoryBuilder() + .setDaemon(false) + .setNameFormat("DB_migration-%d") + .build() + )); + } +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/migration/DatabaseMigrationImpl.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/migration/DatabaseMigrationImpl.java new file mode 100644 index 00000000000..7e5d5ba359f --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/migration/DatabaseMigrationImpl.java @@ -0,0 +1,116 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.db.migration; + +import java.util.Date; +import java.util.concurrent.Semaphore; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; +import org.sonar.core.util.logs.Profiler; +import org.sonar.server.platform.Platform; +import org.sonar.server.platform.db.migration.DatabaseMigrationState.Status; +import org.sonar.server.platform.db.migration.engine.MigrationEngine; +import org.sonar.server.platform.db.migration.step.MigrationStepExecutionException; + +/** + * Handles concurrency to make sure only one DB migration can run at a time. + */ +public class DatabaseMigrationImpl implements DatabaseMigration { + + private static final Logger LOGGER = Loggers.get(DatabaseMigrationImpl.class); + + /** + * ExecutorService implements threads management. + */ + private final DatabaseMigrationExecutorService executorService; + private final MigrationEngine migrationEngine; + private final Platform platform; + private final MutableDatabaseMigrationState migrationState; + /** + * This semaphore implements thread safety from concurrent calls of method {@link #startIt()} + */ + private final Semaphore semaphore = new Semaphore(1); + + public DatabaseMigrationImpl(DatabaseMigrationExecutorService executorService, MutableDatabaseMigrationState migrationState, + MigrationEngine migrationEngine, Platform platform) { + this.executorService = executorService; + this.migrationState = migrationState; + this.migrationEngine = migrationEngine; + this.platform = platform; + } + + @Override + public void startIt() { + if (semaphore.tryAcquire()) { + try { + executorService.execute(this::doDatabaseMigration); + } catch (RuntimeException e) { + semaphore.release(); + throw e; + } + } else { + LOGGER.trace("{}: lock is already taken or process is already running", Thread.currentThread().getName()); + } + } + + private void doDatabaseMigration() { + migrationState.setStatus(Status.RUNNING); + migrationState.setStartedAt(new Date()); + migrationState.setError(null); + Profiler profiler = Profiler.create(LOGGER); + try { + profiler.startInfo("Starting DB Migration and container restart"); + doUpgradeDb(); + doRestartContainer(); + migrationState.setStatus(Status.SUCCEEDED); + profiler.stopInfo("DB Migration and container restart: success"); + } catch (MigrationStepExecutionException e) { + profiler.stopError("DB migration failed"); + LOGGER.error("DB migration ended with an exception", e); + saveStatus(e); + } catch (Throwable t) { + profiler.stopError("Container restart failed"); + LOGGER.error("Container restart failed", t); + saveStatus(t); + } finally { + semaphore.release(); + } + } + + private void saveStatus(Throwable e) { + migrationState.setStatus(Status.FAILED); + migrationState.setError(e); + } + + private void doUpgradeDb() { + Profiler profiler = Profiler.createIfTrace(LOGGER); + profiler.startTrace("Starting DB Migration"); + migrationEngine.execute(); + profiler.stopTrace("DB Migration ended"); + } + + private void doRestartContainer() { + Profiler profiler = Profiler.createIfTrace(LOGGER); + profiler.startTrace("Restarting container"); + platform.doStart(); + profiler.stopTrace("Container restarted successfully"); + } + +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/migration/package-info.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/migration/package-info.java new file mode 100644 index 00000000000..52a8464fb32 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/migration/package-info.java @@ -0,0 +1,23 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +@ParametersAreNonnullByDefault +package org.sonar.server.platform.db.migration; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/package-info.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/package-info.java new file mode 100644 index 00000000000..de34ff5428d --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/package-info.java @@ -0,0 +1,23 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +@ParametersAreNonnullByDefault +package org.sonar.server.platform.db; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/BaseSectionMBean.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/BaseSectionMBean.java new file mode 100644 index 00000000000..c7ff804adf3 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/BaseSectionMBean.java @@ -0,0 +1,56 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.monitoring; + +import org.picocontainer.Startable; +import org.sonar.process.Jmx; +import org.sonar.process.systeminfo.SystemInfoSection; + +/** + * Base implementation of a {@link SystemInfoSection} + * that is exported as a JMX bean + */ +public abstract class BaseSectionMBean implements SystemInfoSection, Startable { + + /** + * Auto-registers to MBean server + */ + @Override + public void start() { + Jmx.register(objectName(), this); + } + + /** + * Unregister, if needed + */ + @Override + public void stop() { + Jmx.unregister(objectName()); + } + + String objectName() { + return "SonarQube:name=" + name(); + } + + /** + * Name of section in System Info page + */ + abstract String name(); +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/DbConnectionSection.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/DbConnectionSection.java new file mode 100644 index 00000000000..871fbab5e62 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/DbConnectionSection.java @@ -0,0 +1,125 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.monitoring; + +import org.apache.commons.dbcp2.BasicDataSource; +import org.sonar.api.SonarQubeSide; +import org.sonar.api.SonarRuntime; +import org.sonar.db.DbClient; +import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo.Section; +import org.sonar.server.platform.db.migration.version.DatabaseVersion; + +import static org.sonar.process.systeminfo.SystemInfoUtils.setAttribute; + +/** + * Information about database connection pool + */ +public class DbConnectionSection extends BaseSectionMBean implements DbConnectionSectionMBean { + + private final DatabaseVersion dbVersion; + private final DbClient dbClient; + private final SonarRuntime runtime; + + public DbConnectionSection(DatabaseVersion dbVersion, DbClient dbClient, SonarRuntime runtime) { + this.dbVersion = dbVersion; + this.dbClient = dbClient; + this.runtime = runtime; + } + + @Override + public String name() { + return "Database"; + } + + @Override + public String getMigrationStatus() { + return dbVersion.getStatus().name(); + } + + @Override + public int getPoolActiveConnections() { + return commonsDbcp().getNumActive(); + } + + @Override + public int getPoolMaxActiveConnections() { + return commonsDbcp().getMaxTotal(); + } + + @Override + public int getPoolIdleConnections() { + return commonsDbcp().getNumIdle(); + } + + @Override + public int getPoolMaxIdleConnections() { + return commonsDbcp().getMaxIdle(); + } + + @Override + public int getPoolMinIdleConnections() { + return commonsDbcp().getMinIdle(); + } + + @Override + public int getPoolInitialSize() { + return commonsDbcp().getInitialSize(); + } + + @Override + public long getPoolMaxWaitMillis() { + return commonsDbcp().getMaxWaitMillis(); + } + + @Override + public boolean getPoolRemoveAbandoned() { + return commonsDbcp().getRemoveAbandonedOnBorrow(); + } + + @Override + public int getPoolRemoveAbandonedTimeoutSeconds() { + return commonsDbcp().getRemoveAbandonedTimeout(); + } + + @Override + public Section toProtobuf() { + Section.Builder protobuf = Section.newBuilder(); + String side = runtime.getSonarQubeSide() == SonarQubeSide.COMPUTE_ENGINE ? "Compute Engine" : "Web"; + protobuf.setName(side + " Database Connection"); + completePoolAttributes(protobuf); + return protobuf.build(); + } + + private void completePoolAttributes(Section.Builder protobuf) { + setAttribute(protobuf, "Pool Active Connections", getPoolActiveConnections()); + setAttribute(protobuf, "Pool Max Connections", getPoolMaxActiveConnections()); + setAttribute(protobuf, "Pool Initial Size", getPoolInitialSize()); + setAttribute(protobuf, "Pool Idle Connections", getPoolIdleConnections()); + setAttribute(protobuf, "Pool Min Idle Connections", getPoolMinIdleConnections()); + setAttribute(protobuf, "Pool Max Idle Connections", getPoolMaxIdleConnections()); + setAttribute(protobuf, "Pool Max Wait (ms)", getPoolMaxWaitMillis()); + setAttribute(protobuf, "Pool Remove Abandoned", getPoolRemoveAbandoned()); + setAttribute(protobuf, "Pool Remove Abandoned Timeout (seconds)", getPoolRemoveAbandonedTimeoutSeconds()); + } + + private BasicDataSource commonsDbcp() { + return (BasicDataSource) dbClient.getDatabase().getDataSource(); + } +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/DbConnectionSectionMBean.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/DbConnectionSectionMBean.java new file mode 100644 index 00000000000..8aca3210e84 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/DbConnectionSectionMBean.java @@ -0,0 +1,71 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.monitoring; + +public interface DbConnectionSectionMBean { + + /** + * Is database schema up-to-date or should it be upgraded ? + */ + String getMigrationStatus(); + + /** + * + */ + int getPoolActiveConnections(); + + /** + * The maximum number of active connections that can be allocated from this pool at the same time, or negative for no limit. + */ + int getPoolMaxActiveConnections(); + + int getPoolIdleConnections(); + + /** + * The maximum number of connections that can remain idle in the pool, without extra ones being released, or negative for no limit. + */ + int getPoolMaxIdleConnections(); + + /** + * The minimum number of connections that can remain idle in the pool, without extra ones being created, or zero to create none. + */ + int getPoolMinIdleConnections(); + + /** + * The initial number of connections that are created when the pool is started. + */ + int getPoolInitialSize(); + + /** + * The maximum number of milliseconds that the pool will wait + * (when there are no available connections) for a connection to be returned before throwing an exception, or -1 to wait indefinitely. + */ + long getPoolMaxWaitMillis(); + + /** + * Flag to remove abandoned connections if they exceed the {@link #getPoolRemoveAbandonedTimeoutSeconds()}. + */ + boolean getPoolRemoveAbandoned(); + + /** + * Timeout in seconds before an abandoned connection can be removed. + */ + int getPoolRemoveAbandonedTimeoutSeconds(); +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/EsIndexesSection.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/EsIndexesSection.java new file mode 100644 index 00000000000..1d07f5a32cd --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/EsIndexesSection.java @@ -0,0 +1,67 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.monitoring; + +import java.util.Map; +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.action.admin.indices.stats.IndexStats; +import org.elasticsearch.action.admin.indices.stats.IndicesStatsResponse; +import org.sonar.api.server.ServerSide; +import org.sonar.api.utils.log.Loggers; +import org.sonar.process.systeminfo.Global; +import org.sonar.process.systeminfo.SystemInfoSection; +import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo; +import org.sonar.server.es.EsClient; + +import static org.apache.commons.io.FileUtils.byteCountToDisplaySize; +import static org.sonar.process.systeminfo.SystemInfoUtils.setAttribute; + +@ServerSide +public class EsIndexesSection implements SystemInfoSection, Global { + + private final EsClient esClient; + + public EsIndexesSection(EsClient esClient) { + this.esClient = esClient; + } + + @Override + public ProtobufSystemInfo.Section toProtobuf() { + ProtobufSystemInfo.Section.Builder protobuf = ProtobufSystemInfo.Section.newBuilder(); + protobuf.setName("Search Indexes"); + try { + completeIndexAttributes(protobuf); + } catch (Exception es) { + Loggers.get(EsIndexesSection.class).warn("Failed to retrieve ES attributes. There will be only a single \"Error\" attribute.", es); + setAttribute(protobuf, "Error", es.getCause() instanceof ElasticsearchException ? es.getCause().getMessage() : es.getMessage()); + } + return protobuf.build(); + } + + private void completeIndexAttributes(ProtobufSystemInfo.Section.Builder protobuf) { + IndicesStatsResponse indicesStats = esClient.prepareStats().all().get(); + for (Map.Entry<String, IndexStats> indexStats : indicesStats.getIndices().entrySet()) { + String prefix = "Index " + indexStats.getKey() + " - "; + setAttribute(protobuf, prefix + "Docs", indexStats.getValue().getPrimaries().getDocs().getCount()); + setAttribute(protobuf, prefix + "Shards", indexStats.getValue().getShards().length); + setAttribute(protobuf, prefix + "Store Size", byteCountToDisplaySize(indexStats.getValue().getPrimaries().getStore().getSizeInBytes())); + } + } +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/EsStateSection.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/EsStateSection.java new file mode 100644 index 00000000000..a865c7e9d0e --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/EsStateSection.java @@ -0,0 +1,106 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.monitoring; + +import java.util.Locale; +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.action.admin.cluster.node.stats.NodeStats; +import org.elasticsearch.action.admin.cluster.node.stats.NodesStatsResponse; +import org.elasticsearch.action.admin.cluster.stats.ClusterStatsResponse; +import org.elasticsearch.cluster.health.ClusterHealthStatus; +import org.elasticsearch.common.breaker.CircuitBreaker; +import org.sonar.api.utils.log.Loggers; +import org.sonar.process.systeminfo.SystemInfoSection; +import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo; +import org.sonar.server.es.EsClient; + +import static java.lang.String.format; +import static org.apache.commons.io.FileUtils.byteCountToDisplaySize; +import static org.sonar.process.systeminfo.SystemInfoUtils.setAttribute; + +public class EsStateSection implements SystemInfoSection { + + private final EsClient esClient; + + public EsStateSection(EsClient esClient) { + this.esClient = esClient; + } + + private ClusterHealthStatus getStateAsEnum() { + return clusterStats().getStatus(); + } + + @Override + public ProtobufSystemInfo.Section toProtobuf() { + ProtobufSystemInfo.Section.Builder protobuf = ProtobufSystemInfo.Section.newBuilder(); + protobuf.setName("Search State"); + try { + setAttribute(protobuf, "State", getStateAsEnum().name()); + completeNodeAttributes(protobuf); + } catch (Exception es) { + Loggers.get(EsStateSection.class).warn("Failed to retrieve ES attributes. There will be only a single \"state\" attribute.", es); + setAttribute(protobuf, "State", es.getCause() instanceof ElasticsearchException ? es.getCause().getMessage() : es.getMessage()); + } + return protobuf.build(); + } + + private void completeNodeAttributes(ProtobufSystemInfo.Section.Builder protobuf) { + NodesStatsResponse nodesStats = esClient.prepareNodesStats() + .setFs(true) + .setProcess(true) + .setJvm(true) + .setIndices(true) + .setBreaker(true) + .get(); + if (!nodesStats.getNodes().isEmpty()) { + NodeStats stats = nodesStats.getNodes().get(0); + toProtobuf(stats, protobuf); + } + } + + public static void toProtobuf(NodeStats stats, ProtobufSystemInfo.Section.Builder protobuf) { + setAttribute(protobuf, "CPU Usage (%)", stats.getProcess().getCpu().getPercent()); + setAttribute(protobuf, "Disk Available", byteCountToDisplaySize(stats.getFs().getTotal().getAvailable().getBytes())); + setAttribute(protobuf, "Store Size", byteCountToDisplaySize(stats.getIndices().getStore().getSizeInBytes())); + setAttribute(protobuf, "Translog Size", byteCountToDisplaySize(stats.getIndices().getTranslog().getTranslogSizeInBytes())); + setAttribute(protobuf, "Open File Descriptors", stats.getProcess().getOpenFileDescriptors()); + setAttribute(protobuf, "Max File Descriptors", stats.getProcess().getMaxFileDescriptors()); + setAttribute(protobuf, "JVM Heap Usage", formatPercent(stats.getJvm().getMem().getHeapUsedPercent())); + setAttribute(protobuf, "JVM Heap Used", byteCountToDisplaySize(stats.getJvm().getMem().getHeapUsed().getBytes())); + setAttribute(protobuf, "JVM Heap Max", byteCountToDisplaySize(stats.getJvm().getMem().getHeapMax().getBytes())); + setAttribute(protobuf, "JVM Non Heap Used", byteCountToDisplaySize(stats.getJvm().getMem().getNonHeapUsed().getBytes())); + setAttribute(protobuf, "JVM Threads", stats.getJvm().getThreads().getCount()); + setAttribute(protobuf, "Field Data Memory", byteCountToDisplaySize(stats.getIndices().getFieldData().getMemorySizeInBytes())); + setAttribute(protobuf, "Field Data Circuit Breaker Limit", byteCountToDisplaySize(stats.getBreaker().getStats(CircuitBreaker.FIELDDATA).getLimit())); + setAttribute(protobuf, "Field Data Circuit Breaker Estimation", byteCountToDisplaySize(stats.getBreaker().getStats(CircuitBreaker.FIELDDATA).getEstimated())); + setAttribute(protobuf, "Request Circuit Breaker Limit", byteCountToDisplaySize(stats.getBreaker().getStats(CircuitBreaker.REQUEST).getLimit())); + setAttribute(protobuf, "Request Circuit Breaker Estimation", byteCountToDisplaySize(stats.getBreaker().getStats(CircuitBreaker.REQUEST).getEstimated())); + setAttribute(protobuf, "Query Cache Memory", byteCountToDisplaySize(stats.getIndices().getQueryCache().getMemorySizeInBytes())); + setAttribute(protobuf, "Request Cache Memory", byteCountToDisplaySize(stats.getIndices().getRequestCache().getMemorySizeInBytes())); + } + + private ClusterStatsResponse clusterStats() { + return esClient.prepareClusterStats().get(); + } + + private static String formatPercent(long amount) { + return format(Locale.ENGLISH, "%.1f%%", 100.0 * amount * 1.0 / 100.0); + } +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/PluginsSection.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/PluginsSection.java new file mode 100644 index 00000000000..4fd9bb19942 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/PluginsSection.java @@ -0,0 +1,53 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.monitoring; + +import org.sonar.api.server.ServerSide; +import org.sonar.core.platform.PluginInfo; +import org.sonar.core.platform.PluginRepository; +import org.sonar.process.systeminfo.SystemInfoSection; +import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo; +import org.sonar.updatecenter.common.Version; + +import static org.sonar.process.systeminfo.SystemInfoUtils.setAttribute; + +@ServerSide +public class PluginsSection implements SystemInfoSection { + private final PluginRepository repository; + + public PluginsSection(PluginRepository repository) { + this.repository = repository; + } + + @Override + public ProtobufSystemInfo.Section toProtobuf() { + ProtobufSystemInfo.Section.Builder protobuf = ProtobufSystemInfo.Section.newBuilder(); + protobuf.setName("Plugins"); + for (PluginInfo plugin : repository.getPluginInfos()) { + String label = "[" + plugin.getName() + "]"; + Version version = plugin.getVersion(); + if (version != null) { + label = version.getName() + " " + label; + } + setAttribute(protobuf, plugin.getKey(), label); + } + return protobuf.build(); + } +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/SettingsSection.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/SettingsSection.java new file mode 100644 index 00000000000..daa26a5e6f7 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/SettingsSection.java @@ -0,0 +1,78 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.monitoring; + +import java.util.Map; +import java.util.TreeMap; +import org.sonar.api.PropertyType; +import org.sonar.api.config.PropertyDefinition; +import org.sonar.api.config.PropertyDefinitions; +import org.sonar.api.config.Settings; +import org.sonar.api.server.ServerSide; +import org.sonar.process.systeminfo.Global; +import org.sonar.process.systeminfo.SystemInfoSection; +import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo; + +import static org.apache.commons.lang.StringUtils.abbreviate; +import static org.apache.commons.lang.StringUtils.containsIgnoreCase; +import static org.apache.commons.lang.StringUtils.endsWithIgnoreCase; +import static org.sonar.process.ProcessProperties.Property.AUTH_JWT_SECRET; +import static org.sonar.process.systeminfo.SystemInfoUtils.setAttribute; + +@ServerSide +public class SettingsSection implements SystemInfoSection, Global { + + private static final int MAX_VALUE_LENGTH = 500; + private static final String PASSWORD_VALUE = "xxxxxxxx"; + private final Settings settings; + + public SettingsSection(Settings settings) { + this.settings = settings; + } + + @Override + public ProtobufSystemInfo.Section toProtobuf() { + ProtobufSystemInfo.Section.Builder protobuf = ProtobufSystemInfo.Section.newBuilder(); + protobuf.setName("Settings"); + + PropertyDefinitions definitions = settings.getDefinitions(); + TreeMap<String, String> orderedProps = new TreeMap<>(settings.getProperties()); + for (Map.Entry<String, String> prop : orderedProps.entrySet()) { + String key = prop.getKey(); + String value = obfuscateValue(definitions, key, prop.getValue()); + setAttribute(protobuf, key, value); + } + return protobuf.build(); + } + + private static String obfuscateValue(PropertyDefinitions definitions, String key, String value) { + PropertyDefinition def = definitions.get(key); + if (def != null && def.type() == PropertyType.PASSWORD) { + return PASSWORD_VALUE; + } + if (endsWithIgnoreCase(key, ".secured") || + containsIgnoreCase(key, "password") || + containsIgnoreCase(key, "passcode") || + AUTH_JWT_SECRET.getKey().equals(key)) { + return PASSWORD_VALUE; + } + return abbreviate(value, MAX_VALUE_LENGTH); + } +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/StandaloneSystemSection.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/StandaloneSystemSection.java new file mode 100644 index 00000000000..1454ae298b0 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/StandaloneSystemSection.java @@ -0,0 +1,138 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.monitoring; + +import com.google.common.base.Joiner; +import java.util.List; +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; +import org.sonar.api.CoreProperties; +import org.sonar.api.config.Configuration; +import org.sonar.api.platform.Server; +import org.sonar.api.security.SecurityRealm; +import org.sonar.api.server.authentication.IdentityProvider; +import org.sonar.core.util.stream.MoreCollectors; +import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo; +import org.sonar.server.authentication.IdentityProviderRepository; +import org.sonar.server.log.ServerLogging; +import org.sonar.server.platform.OfficialDistribution; +import org.sonar.server.user.SecurityRealmFactory; + +import static org.sonar.process.ProcessProperties.Property.PATH_DATA; +import static org.sonar.process.ProcessProperties.Property.PATH_HOME; +import static org.sonar.process.ProcessProperties.Property.PATH_TEMP; +import static org.sonar.process.systeminfo.SystemInfoUtils.setAttribute; + +public class StandaloneSystemSection extends BaseSectionMBean implements SystemSectionMBean { + + private static final Joiner COMMA_JOINER = Joiner.on(", "); + + private final Configuration config; + private final SecurityRealmFactory securityRealmFactory; + private final IdentityProviderRepository identityProviderRepository; + private final Server server; + private final ServerLogging serverLogging; + private final OfficialDistribution officialDistribution; + + public StandaloneSystemSection(Configuration config, SecurityRealmFactory securityRealmFactory, + IdentityProviderRepository identityProviderRepository, Server server, ServerLogging serverLogging, + OfficialDistribution officialDistribution) { + this.config = config; + this.securityRealmFactory = securityRealmFactory; + this.identityProviderRepository = identityProviderRepository; + this.server = server; + this.serverLogging = serverLogging; + this.officialDistribution = officialDistribution; + } + + @Override + public String getServerId() { + return server.getId(); + } + + @Override + public String getVersion() { + return server.getVersion(); + } + + @Override + public String getLogLevel() { + return serverLogging.getRootLoggerLevel().name(); + } + + @CheckForNull + private String getExternalUserAuthentication() { + SecurityRealm realm = securityRealmFactory.getRealm(); + return realm == null ? null : realm.getName(); + } + + private List<String> getEnabledIdentityProviders() { + return identityProviderRepository.getAllEnabledAndSorted() + .stream() + .filter(IdentityProvider::isEnabled) + .map(IdentityProvider::getName) + .collect(MoreCollectors.toList()); + } + + private List<String> getAllowsToSignUpEnabledIdentityProviders() { + return identityProviderRepository.getAllEnabledAndSorted() + .stream() + .filter(IdentityProvider::isEnabled) + .filter(IdentityProvider::allowsUsersToSignUp) + .map(IdentityProvider::getName) + .collect(MoreCollectors.toList()); + } + + private boolean getForceAuthentication() { + return config.getBoolean(CoreProperties.CORE_FORCE_AUTHENTICATION_PROPERTY).orElse(false); + } + + @Override + public String name() { + // JMX name + return "SonarQube"; + } + + @Override + public ProtobufSystemInfo.Section toProtobuf() { + ProtobufSystemInfo.Section.Builder protobuf = ProtobufSystemInfo.Section.newBuilder(); + protobuf.setName("System"); + + setAttribute(protobuf, "Server ID", server.getId()); + setAttribute(protobuf, "Version", getVersion()); + setAttribute(protobuf, "External User Authentication", getExternalUserAuthentication()); + addIfNotEmpty(protobuf, "Accepted external identity providers", getEnabledIdentityProviders()); + addIfNotEmpty(protobuf, "External identity providers whose users are allowed to sign themselves up", getAllowsToSignUpEnabledIdentityProviders()); + setAttribute(protobuf, "High Availability", false); + setAttribute(protobuf, "Official Distribution", officialDistribution.check()); + setAttribute(protobuf, "Force authentication", getForceAuthentication()); + setAttribute(protobuf, "Home Dir", config.get(PATH_HOME.getKey()).orElse(null)); + setAttribute(protobuf, "Data Dir", config.get(PATH_DATA.getKey()).orElse(null)); + setAttribute(protobuf, "Temp Dir", config.get(PATH_TEMP.getKey()).orElse(null)); + setAttribute(protobuf, "Processors", Runtime.getRuntime().availableProcessors()); + return protobuf.build(); + } + + private static void addIfNotEmpty(ProtobufSystemInfo.Section.Builder protobuf, String key, @Nullable List<String> values) { + if (values != null && !values.isEmpty()) { + setAttribute(protobuf, key, COMMA_JOINER.join(values)); + } + } +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/SystemSectionMBean.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/SystemSectionMBean.java new file mode 100644 index 00000000000..07cc22074c0 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/SystemSectionMBean.java @@ -0,0 +1,31 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.monitoring; + +import javax.annotation.CheckForNull; + +public interface SystemSectionMBean { + @CheckForNull + String getServerId(); + + String getVersion(); + + String getLogLevel(); +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/AppNodesInfoLoader.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/AppNodesInfoLoader.java new file mode 100644 index 00000000000..cea4044190b --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/AppNodesInfoLoader.java @@ -0,0 +1,27 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.monitoring.cluster; + +import java.util.Collection; + +public interface AppNodesInfoLoader { + + Collection<NodeInfo> load() throws InterruptedException; +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/AppNodesInfoLoaderImpl.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/AppNodesInfoLoaderImpl.java new file mode 100644 index 00000000000..3fdccddf5e9 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/AppNodesInfoLoaderImpl.java @@ -0,0 +1,82 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.monitoring.cluster; + +import com.hazelcast.core.Member; +import com.hazelcast.core.MemberSelector; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import org.sonar.api.server.ServerSide; +import org.sonar.process.ProcessId; +import org.sonar.process.cluster.hz.DistributedAnswer; +import org.sonar.process.cluster.hz.HazelcastMember; +import org.sonar.process.cluster.hz.HazelcastMemberSelectors; +import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo; + +import static org.sonar.process.cluster.hz.HazelcastMember.Attribute.NODE_NAME; + +@ServerSide +public class AppNodesInfoLoaderImpl implements AppNodesInfoLoader { + + /** + * Timeout to get information from all nodes + */ + private static final long DISTRIBUTED_TIMEOUT_MS = 15_000L; + + private final HazelcastMember hzMember; + + public AppNodesInfoLoaderImpl(HazelcastMember hzMember) { + this.hzMember = hzMember; + } + + public AppNodesInfoLoaderImpl() { + this(null); + } + + public Collection<NodeInfo> load() throws InterruptedException { + Map<String, NodeInfo> nodesByName = new HashMap<>(); + MemberSelector memberSelector = HazelcastMemberSelectors.selectorForProcessIds(ProcessId.WEB_SERVER, ProcessId.COMPUTE_ENGINE); + DistributedAnswer<ProtobufSystemInfo.SystemInfo> distributedAnswer = hzMember.call(ProcessInfoProvider::provide, memberSelector, DISTRIBUTED_TIMEOUT_MS); + for (Member member : distributedAnswer.getMembers()) { + String nodeName = member.getStringAttribute(NODE_NAME.getKey()); + NodeInfo nodeInfo = nodesByName.computeIfAbsent(nodeName, name -> { + NodeInfo info = new NodeInfo(name); + info.setHost(member.getAddress().getHost()); + return info; + }); + completeNodeInfo(distributedAnswer, member, nodeInfo); + } + return nodesByName.values(); + } + + private static void completeNodeInfo(DistributedAnswer<ProtobufSystemInfo.SystemInfo> distributedAnswer, Member member, NodeInfo nodeInfo) { + Optional<ProtobufSystemInfo.SystemInfo> nodeAnswer = distributedAnswer.getAnswer(member); + Optional<Exception> failure = distributedAnswer.getFailed(member); + if (distributedAnswer.hasTimedOut(member)) { + nodeInfo.setErrorMessage("Failed to retrieve information on time"); + } else if (failure.isPresent()) { + nodeInfo.setErrorMessage("Failed to retrieve information: " + failure.get().getMessage()); + } else if (nodeAnswer.isPresent()) { + nodeAnswer.get().getSectionsList().forEach(nodeInfo::addSection); + } + } +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/CeQueueGlobalSection.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/CeQueueGlobalSection.java new file mode 100644 index 00000000000..5d05864ea23 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/CeQueueGlobalSection.java @@ -0,0 +1,65 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.monitoring.cluster; + +import javax.annotation.Nullable; +import org.sonar.api.server.ServerSide; +import org.sonar.ce.configuration.WorkerCountProvider; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.ce.CeQueueDto; +import org.sonar.process.systeminfo.Global; +import org.sonar.process.systeminfo.SystemInfoSection; +import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo; +import org.sonar.server.property.InternalProperties; + +import static org.sonar.process.systeminfo.SystemInfoUtils.setAttribute; + +@ServerSide +public class CeQueueGlobalSection implements SystemInfoSection, Global { + + private static final int DEFAULT_NB_OF_WORKERS = 1; + + private final DbClient dbClient; + @Nullable + private final WorkerCountProvider workerCountProvider; + + public CeQueueGlobalSection(DbClient dbClient, @Nullable WorkerCountProvider workerCountProvider) { + this.dbClient = dbClient; + this.workerCountProvider = workerCountProvider; + } + + public CeQueueGlobalSection(DbClient dbClient) { + this(dbClient, null); + } + + @Override + public ProtobufSystemInfo.Section toProtobuf() { + ProtobufSystemInfo.Section.Builder protobuf = ProtobufSystemInfo.Section.newBuilder(); + protobuf.setName("Compute Engine Tasks"); + try (DbSession dbSession = dbClient.openSession(false)) { + setAttribute(protobuf, "Total Pending", dbClient.ceQueueDao().countByStatus(dbSession, CeQueueDto.Status.PENDING)); + setAttribute(protobuf, "Total In Progress", dbClient.ceQueueDao().countByStatus(dbSession, CeQueueDto.Status.IN_PROGRESS)); + setAttribute(protobuf, "Max Workers per Node", workerCountProvider == null ? DEFAULT_NB_OF_WORKERS : workerCountProvider.get()); + setAttribute(protobuf, "Workers Paused", "true".equals(dbClient.internalPropertiesDao().selectByKey(dbSession, InternalProperties.COMPUTE_ENGINE_PAUSE).orElse(null))); + } + return protobuf.build(); + } +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/EsClusterStateSection.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/EsClusterStateSection.java new file mode 100644 index 00000000000..e257de37c9f --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/EsClusterStateSection.java @@ -0,0 +1,54 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.monitoring.cluster; + +import org.elasticsearch.action.admin.cluster.stats.ClusterStatsResponse; +import org.sonar.api.server.ServerSide; +import org.sonar.process.systeminfo.Global; +import org.sonar.process.systeminfo.SystemInfoSection; +import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo; +import org.sonar.server.es.EsClient; + +import static org.sonar.process.systeminfo.SystemInfoUtils.setAttribute; + +/** + * In cluster mode, section "Search" that displays all ES information + * that are not specific to a node or an index + */ +@ServerSide +public class EsClusterStateSection implements SystemInfoSection, Global { + + private final EsClient esClient; + + public EsClusterStateSection(EsClient esClient) { + this.esClient = esClient; + } + + @Override + public ProtobufSystemInfo.Section toProtobuf() { + ProtobufSystemInfo.Section.Builder protobuf = ProtobufSystemInfo.Section.newBuilder(); + protobuf.setName("Search State"); + ClusterStatsResponse stats = esClient.prepareClusterStats().get(); + setAttribute(protobuf, "State", stats.getStatus().name()); + setAttribute(protobuf, "Nodes", stats.getNodesStats().getCounts().getTotal()); + return protobuf.build(); + } + +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/GlobalInfoLoader.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/GlobalInfoLoader.java new file mode 100644 index 00000000000..6cfde83913a --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/GlobalInfoLoader.java @@ -0,0 +1,45 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.monitoring.cluster; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import org.sonar.api.server.ServerSide; +import org.sonar.process.systeminfo.Global; +import org.sonar.process.systeminfo.SystemInfoSection; +import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo; + +@ServerSide +public class GlobalInfoLoader { + private final List<SystemInfoSection> globalSections; + + public GlobalInfoLoader(SystemInfoSection[] sections) { + this.globalSections = Arrays.stream(sections) + .filter(section -> section instanceof Global) + .collect(Collectors.toList()); + } + + public List<ProtobufSystemInfo.Section> load() { + return globalSections.stream() + .map(SystemInfoSection::toProtobuf) + .collect(Collectors.toList()); + } +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/GlobalSystemSection.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/GlobalSystemSection.java new file mode 100644 index 00000000000..6ebb83d06f0 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/GlobalSystemSection.java @@ -0,0 +1,105 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.monitoring.cluster; + +import com.google.common.base.Joiner; +import java.util.List; +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; +import org.sonar.api.CoreProperties; +import org.sonar.api.config.Configuration; +import org.sonar.api.platform.Server; +import org.sonar.api.security.SecurityRealm; +import org.sonar.api.server.ServerSide; +import org.sonar.api.server.authentication.IdentityProvider; +import org.sonar.core.util.stream.MoreCollectors; +import org.sonar.process.systeminfo.Global; +import org.sonar.process.systeminfo.SystemInfoSection; +import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo; +import org.sonar.server.authentication.IdentityProviderRepository; +import org.sonar.server.user.SecurityRealmFactory; + +import static org.sonar.process.systeminfo.SystemInfoUtils.setAttribute; + +@ServerSide +public class GlobalSystemSection implements SystemInfoSection, Global { + private static final Joiner COMMA_JOINER = Joiner.on(", "); + + private final Configuration config; + private final Server server; + private final SecurityRealmFactory securityRealmFactory; + private final IdentityProviderRepository identityProviderRepository; + + public GlobalSystemSection(Configuration config, Server server, SecurityRealmFactory securityRealmFactory, + IdentityProviderRepository identityProviderRepository) { + this.config = config; + this.server = server; + this.securityRealmFactory = securityRealmFactory; + this.identityProviderRepository = identityProviderRepository; + } + + @Override + public ProtobufSystemInfo.Section toProtobuf() { + ProtobufSystemInfo.Section.Builder protobuf = ProtobufSystemInfo.Section.newBuilder(); + protobuf.setName("System"); + + setAttribute(protobuf, "Server ID", server.getId()); + setAttribute(protobuf, "High Availability", true); + setAttribute(protobuf, "External User Authentication", getExternalUserAuthentication()); + addIfNotEmpty(protobuf, "Accepted external identity providers", getEnabledIdentityProviders()); + addIfNotEmpty(protobuf, "External identity providers whose users are allowed to sign themselves up", getAllowsToSignUpEnabledIdentityProviders()); + setAttribute(protobuf, "Force authentication", getForceAuthentication()); + return protobuf.build(); + } + + private List<String> getEnabledIdentityProviders() { + return identityProviderRepository.getAllEnabledAndSorted() + .stream() + .filter(IdentityProvider::isEnabled) + .map(IdentityProvider::getName) + .collect(MoreCollectors.toList()); + } + + private List<String> getAllowsToSignUpEnabledIdentityProviders() { + return identityProviderRepository.getAllEnabledAndSorted() + .stream() + .filter(IdentityProvider::isEnabled) + .filter(IdentityProvider::allowsUsersToSignUp) + .map(IdentityProvider::getName) + .collect(MoreCollectors.toList()); + } + + private boolean getForceAuthentication() { + return config.getBoolean(CoreProperties.CORE_FORCE_AUTHENTICATION_PROPERTY).orElse(false); + } + + private static void addIfNotEmpty(ProtobufSystemInfo.Section.Builder protobuf, String key, @Nullable List<String> values) { + if (values != null && !values.isEmpty()) { + setAttribute(protobuf, key, COMMA_JOINER.join(values)); + } + } + + @CheckForNull + private String getExternalUserAuthentication() { + SecurityRealm realm = securityRealmFactory.getRealm(); + return realm == null ? null : realm.getName(); + } + +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/NodeInfo.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/NodeInfo.java new file mode 100644 index 00000000000..d60be12979e --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/NodeInfo.java @@ -0,0 +1,99 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.monitoring.cluster; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import javax.annotation.Nullable; +import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo; + +/** + * Represents the system information of a cluster node. In the case of + * application node, it merges information from Web Server and Compute + * Engine processes. + * + */ +public class NodeInfo { + + private final String name; + private String host = null; + private Long startedAt = null; + private String errorMessage = null; + private final List<ProtobufSystemInfo.Section> sections = new ArrayList<>(); + + public NodeInfo(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public Optional<String> getHost() { + return Optional.ofNullable(host); + } + + public void setHost(@Nullable String s) { + this.host = s; + } + + public Optional<Long> getStartedAt() { + return Optional.ofNullable(startedAt); + } + + public void setStartedAt(@Nullable Long l) { + this.startedAt = l; + } + + public Optional<String> getErrorMessage() { + return Optional.ofNullable(errorMessage); + } + + public void setErrorMessage(@Nullable String s) { + this.errorMessage = s; + } + + public NodeInfo addSection(ProtobufSystemInfo.Section section) { + this.sections.add(section); + return this; + } + + public List<ProtobufSystemInfo.Section> getSections() { + return sections; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + NodeInfo nodeInfo = (NodeInfo) o; + return name.equals(nodeInfo.name); + } + + @Override + public int hashCode() { + return name.hashCode(); + } +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/NodeSystemSection.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/NodeSystemSection.java new file mode 100644 index 00000000000..0960336bdaf --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/NodeSystemSection.java @@ -0,0 +1,61 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.monitoring.cluster; + +import org.sonar.api.config.Configuration; +import org.sonar.api.platform.Server; +import org.sonar.api.server.ServerSide; +import org.sonar.process.systeminfo.SystemInfoSection; +import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo; +import org.sonar.server.platform.OfficialDistribution; + +import static org.sonar.process.ProcessProperties.Property.PATH_DATA; +import static org.sonar.process.ProcessProperties.Property.PATH_HOME; +import static org.sonar.process.ProcessProperties.Property.PATH_TEMP; +import static org.sonar.process.systeminfo.SystemInfoUtils.setAttribute; + +@ServerSide +public class NodeSystemSection implements SystemInfoSection { + + private final Configuration config; + private final Server server; + private final OfficialDistribution officialDistribution; + + public NodeSystemSection(Configuration config, Server server, OfficialDistribution officialDistribution) { + this.config = config; + this.server = server; + this.officialDistribution = officialDistribution; + } + + @Override + public ProtobufSystemInfo.Section toProtobuf() { + ProtobufSystemInfo.Section.Builder protobuf = ProtobufSystemInfo.Section.newBuilder(); + protobuf.setName("System"); + + setAttribute(protobuf, "Version", server.getVersion()); + setAttribute(protobuf, "Official Distribution", officialDistribution.check()); + setAttribute(protobuf, "Home Dir", config.get(PATH_HOME.getKey()).orElse(null)); + setAttribute(protobuf, "Data Dir", config.get(PATH_DATA.getKey()).orElse(null)); + setAttribute(protobuf, "Temp Dir", config.get(PATH_TEMP.getKey()).orElse(null)); + setAttribute(protobuf, "Processors", Runtime.getRuntime().availableProcessors()); + return protobuf.build(); + } + +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/SearchNodesInfoLoader.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/SearchNodesInfoLoader.java new file mode 100644 index 00000000000..2c17b9666ab --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/SearchNodesInfoLoader.java @@ -0,0 +1,30 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.monitoring.cluster; + + +import java.util.Collection; + +/** + * Loads "system information" of all Elasticsearch nodes. + */ +public interface SearchNodesInfoLoader { + Collection<NodeInfo> load(); +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/SearchNodesInfoLoaderImpl.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/SearchNodesInfoLoaderImpl.java new file mode 100644 index 00000000000..5ce39d0d9f2 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/SearchNodesInfoLoaderImpl.java @@ -0,0 +1,67 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.monitoring.cluster; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import org.elasticsearch.action.admin.cluster.node.stats.NodeStats; +import org.elasticsearch.action.admin.cluster.node.stats.NodesStatsResponse; +import org.sonar.api.server.ServerSide; +import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo; +import org.sonar.server.es.EsClient; +import org.sonar.server.platform.monitoring.EsStateSection; + +@ServerSide +public class SearchNodesInfoLoaderImpl implements SearchNodesInfoLoader { + + private final EsClient esClient; + + public SearchNodesInfoLoaderImpl(EsClient esClient) { + this.esClient = esClient; + } + + public Collection<NodeInfo> load() { + NodesStatsResponse nodesStats = esClient.prepareNodesStats() + .setFs(true) + .setProcess(true) + .setJvm(true) + .setIndices(true) + .setBreaker(true) + .get(); + List<NodeInfo> result = new ArrayList<>(); + nodesStats.getNodes().forEach(nodeStat -> result.add(toNodeInfo(nodeStat))); + return result; + } + + private static NodeInfo toNodeInfo(NodeStats stat) { + String nodeName = stat.getNode().getName(); + NodeInfo info = new NodeInfo(nodeName); + info.setHost(stat.getHostname()); + + ProtobufSystemInfo.Section.Builder section = ProtobufSystemInfo.Section.newBuilder(); + section.setName("Search State"); + EsStateSection.toProtobuf(stat, section); + info.addSection(section.build()); + + return info; + } + +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/package-info.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/package-info.java new file mode 100644 index 00000000000..45cea3a7a14 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/cluster/package-info.java @@ -0,0 +1,23 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +@ParametersAreNonnullByDefault +package org.sonar.server.platform.monitoring.cluster; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/package-info.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/package-info.java new file mode 100644 index 00000000000..895638cb3f2 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/monitoring/package-info.java @@ -0,0 +1,23 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +@ParametersAreNonnullByDefault +package org.sonar.server.platform.monitoring; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/package-info.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/package-info.java new file mode 100644 index 00000000000..71d187381a7 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/package-info.java @@ -0,0 +1,23 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +@ParametersAreNonnullByDefault +package org.sonar.server.platform; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/serverid/ServerIdFactory.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/serverid/ServerIdFactory.java new file mode 100644 index 00000000000..e62ef12f2b5 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/serverid/ServerIdFactory.java @@ -0,0 +1,34 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.serverid; + +import org.sonar.core.platform.ServerId; + +public interface ServerIdFactory { + /** + * Create a new ServerId from scratch. + */ + ServerId create(); + + /** + * Create a new ServerId from the current serverId. + */ + ServerId create(ServerId currentServerId); +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/serverid/ServerIdFactoryImpl.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/serverid/ServerIdFactoryImpl.java new file mode 100644 index 00000000000..8bee91eace9 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/serverid/ServerIdFactoryImpl.java @@ -0,0 +1,69 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.serverid; + +import com.google.common.annotations.VisibleForTesting; +import java.nio.charset.StandardCharsets; +import java.util.Locale; +import java.util.zip.CRC32; +import org.apache.commons.lang.StringUtils; +import org.sonar.api.config.Configuration; +import org.sonar.core.platform.ServerId; +import org.sonar.core.util.UuidFactory; + +import static org.sonar.process.ProcessProperties.Property.JDBC_URL; + +public class ServerIdFactoryImpl implements ServerIdFactory { + + private final Configuration config; + private final UuidFactory uuidFactory; + private final JdbcUrlSanitizer jdbcUrlSanitizer; + + public ServerIdFactoryImpl(Configuration config, UuidFactory uuidFactory, JdbcUrlSanitizer jdbcUrlSanitizer) { + this.config = config; + this.uuidFactory = uuidFactory; + this.jdbcUrlSanitizer = jdbcUrlSanitizer; + } + + @Override + public ServerId create() { + return ServerId.of(computeDatabaseId(), uuidFactory.create()); + } + + @Override + public ServerId create(ServerId currentServerId) { + return ServerId.of(computeDatabaseId(), currentServerId.getDatasetId()); + } + + private String computeDatabaseId() { + String jdbcUrl = config.get(JDBC_URL.getKey()).orElseThrow(() -> new IllegalStateException("Missing JDBC URL")); + return crc32Hex(jdbcUrlSanitizer.sanitize(jdbcUrl)); + } + + @VisibleForTesting + static String crc32Hex(String str) { + CRC32 crc32 = new CRC32(); + crc32.update(str.getBytes(StandardCharsets.UTF_8)); + long hash = crc32.getValue(); + String s = Long.toHexString(hash).toUpperCase(Locale.ENGLISH); + return StringUtils.leftPad(s, ServerId.DATABASE_ID_LENGTH, "0"); + } + +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/serverid/ServerIdManager.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/serverid/ServerIdManager.java new file mode 100644 index 00000000000..5a4945150b0 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/serverid/ServerIdManager.java @@ -0,0 +1,163 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.serverid; + +import java.util.Optional; +import org.picocontainer.Startable; +import org.sonar.api.SonarQubeSide; +import org.sonar.api.SonarRuntime; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; +import org.sonar.core.platform.ServerId; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.property.PropertyDto; +import org.sonar.server.platform.WebServer; +import org.sonar.server.property.InternalProperties; + +import static com.google.common.base.Preconditions.checkState; +import static org.apache.commons.lang.StringUtils.isEmpty; +import static org.apache.commons.lang.StringUtils.isNotEmpty; +import static org.sonar.api.CoreProperties.SERVER_ID; +import static org.sonar.core.platform.ServerId.Format.DEPRECATED; +import static org.sonar.core.platform.ServerId.Format.NO_DATABASE_ID; +import static org.sonar.server.property.InternalProperties.SERVER_ID_CHECKSUM; + +public class ServerIdManager implements Startable { + private static final Logger LOGGER = Loggers.get(ServerIdManager.class); + + private final ServerIdChecksum serverIdChecksum; + private final ServerIdFactory serverIdFactory; + private final DbClient dbClient; + private final SonarRuntime runtime; + private final WebServer webServer; + + public ServerIdManager(ServerIdChecksum serverIdChecksum, ServerIdFactory serverIdFactory, DbClient dbClient, SonarRuntime runtime, WebServer webServer) { + this.serverIdChecksum = serverIdChecksum; + this.serverIdFactory = serverIdFactory; + this.dbClient = dbClient; + this.runtime = runtime; + this.webServer = webServer; + } + + @Override + public void start() { + try (DbSession dbSession = dbClient.openSession(false)) { + if (runtime.getSonarQubeSide() == SonarQubeSide.SERVER && webServer.isStartupLeader()) { + Optional<String> checksum = dbClient.internalPropertiesDao().selectByKey(dbSession, SERVER_ID_CHECKSUM); + + ServerId serverId = readCurrentServerId(dbSession) + .map(currentServerId -> keepOrReplaceCurrentServerId(dbSession, currentServerId, checksum)) + .orElseGet(() -> createFirstServerId(dbSession)); + updateChecksum(dbSession, serverId); + + dbSession.commit(); + } else { + ensureServerIdIsValid(dbSession); + } + } + } + + private ServerId keepOrReplaceCurrentServerId(DbSession dbSession, ServerId currentServerId, Optional<String> checksum) { + if (keepServerId(currentServerId, checksum)) { + return currentServerId; + } + + ServerId serverId = replaceCurrentServerId(currentServerId); + persistServerId(dbSession, serverId); + return serverId; + } + + private boolean keepServerId(ServerId serverId, Optional<String> checksum) { + ServerId.Format format = serverId.getFormat(); + if (format == DEPRECATED || format == NO_DATABASE_ID) { + LOGGER.info("Server ID is changed to new format."); + return false; + } + + if (checksum.isPresent()) { + String expectedChecksum = serverIdChecksum.computeFor(serverId.toString()); + if (!expectedChecksum.equals(checksum.get())) { + LOGGER.warn("Server ID is reset because it is not valid anymore. Database URL probably changed. The new server ID affects SonarSource licensed products."); + return false; + } + } + + // Existing server ID must be kept when upgrading to 6.7+. In that case the checksum does not exist. + return true; + } + + private ServerId replaceCurrentServerId(ServerId currentServerId) { + if (currentServerId.getFormat() == DEPRECATED) { + return serverIdFactory.create(); + } + return serverIdFactory.create(currentServerId); + } + + private ServerId createFirstServerId(DbSession dbSession) { + ServerId serverId = serverIdFactory.create(); + persistServerId(dbSession, serverId); + return serverId; + } + + private Optional<ServerId> readCurrentServerId(DbSession dbSession) { + PropertyDto dto = dbClient.propertiesDao().selectGlobalProperty(dbSession, SERVER_ID); + if (dto == null) { + return Optional.empty(); + } + + String value = dto.getValue(); + if (isEmpty(value)) { + return Optional.empty(); + } + + return Optional.of(ServerId.parse(value)); + } + + private void updateChecksum(DbSession dbSession, ServerId serverId) { + // checksum must be generated when it does not exist (upgrading to 6.7 or greater) + // or when server ID changed. + String checksum = serverIdChecksum.computeFor(serverId.toString()); + persistChecksum(dbSession, checksum); + } + + private void persistServerId(DbSession dbSession, ServerId serverId) { + dbClient.propertiesDao().saveProperty(dbSession, new PropertyDto().setKey(SERVER_ID).setValue(serverId.toString())); + } + + private void persistChecksum(DbSession dbSession, String checksump) { + dbClient.internalPropertiesDao().save(dbSession, InternalProperties.SERVER_ID_CHECKSUM, checksump); + } + + private void ensureServerIdIsValid(DbSession dbSession) { + PropertyDto id = dbClient.propertiesDao().selectGlobalProperty(dbSession, SERVER_ID); + checkState(id != null, "Property %s is missing in database", SERVER_ID); + checkState(isNotEmpty(id.getValue()), "Property %s is empty in database", SERVER_ID); + + Optional<String> checksum = dbClient.internalPropertiesDao().selectByKey(dbSession, SERVER_ID_CHECKSUM); + checkState(checksum.isPresent(), "Internal property %s is missing in database", SERVER_ID_CHECKSUM); + checkState(checksum.get().equals(serverIdChecksum.computeFor(id.getValue())), "Server ID is invalid"); + } + + @Override + public void stop() { + // nothing to do + } +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/serverid/ServerIdModule.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/serverid/ServerIdModule.java new file mode 100644 index 00000000000..1f01a12704e --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/serverid/ServerIdModule.java @@ -0,0 +1,35 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.serverid; + +import org.sonar.core.platform.Module; + +public class ServerIdModule extends Module { + @Override + protected void configureModule() { + add( + ServerIdFactoryImpl.class, + JdbcUrlSanitizer.class, + ServerIdChecksum.class, + ServerIdManager.class + + ); + } +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/serverid/package-info.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/serverid/package-info.java new file mode 100644 index 00000000000..e7b4b5d7659 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/serverid/package-info.java @@ -0,0 +1,23 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +@ParametersAreNonnullByDefault +package org.sonar.server.platform.serverid; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/AbortTomcatStartException.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/AbortTomcatStartException.java new file mode 100644 index 00000000000..66aa523acf2 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/AbortTomcatStartException.java @@ -0,0 +1,45 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.web; + +/** + * An exception without any stacktrace nor message which point is solely to make tomcat startup fail during + * initialization of the Context Listener {@link PlatformServletContextListener}. + */ +public class AbortTomcatStartException extends RuntimeException { + public AbortTomcatStartException() { + super("Aborting tomcat servlet context startup"); + } + + /** + * Does not fill in the stack trace + * + * @see Throwable#fillInStackTrace() + */ + @Override + public synchronized Throwable fillInStackTrace() { + return this; + } + + @Override + public String toString() { + return getMessage(); + } +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/RootFilter.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/RootFilter.java new file mode 100644 index 00000000000..572444e5414 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/RootFilter.java @@ -0,0 +1,116 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.web; + +import com.google.common.annotations.VisibleForTesting; +import java.io.IOException; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import org.sonar.api.utils.log.Loggers; + +import static java.lang.String.format; + +/** + * <p>Profile HTTP requests using platform profiling utility.</p> + * <p>To avoid profiling of requests for static resources, the <code>staticDirs</code> + * filter parameter can be set in the servlet context descriptor. This parameter should + * contain a comma-separated list of paths, starting at the context root; + * requests on subpaths of these paths will not be profiled.</p> + * + * @since 4.1 + */ +public class RootFilter implements Filter { + + private static final org.sonar.api.utils.log.Logger LOGGER = Loggers.get(RootFilter.class); + + @Override + public void init(FilterConfig filterConfig) { + // nothing to do + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + if (request instanceof HttpServletRequest) { + HttpServletRequest httpRequest = (HttpServletRequest) request; + HttpServletResponse httpResponse = (HttpServletResponse) response; + try { + chain.doFilter(new ServletRequestWrapper(httpRequest), httpResponse); + } catch (Throwable e) { + if (httpResponse.isCommitted()) { + // Request has been aborted by the client, nothing can been done as Tomcat has committed the response + LOGGER.debug(format("Processing of request %s failed", toUrl(httpRequest)), e); + return; + } + LOGGER.error(format("Processing of request %s failed", toUrl(httpRequest)), e); + httpResponse.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + } else { + // Not an HTTP request, not profiled + chain.doFilter(request, response); + } + } + + private static String toUrl(HttpServletRequest request) { + String requestURI = request.getRequestURI(); + String queryString = request.getQueryString(); + if (queryString == null) { + return requestURI; + } + return requestURI + '?' + queryString; + } + + @Override + public void destroy() { + // Nothing + } + + @VisibleForTesting + static class ServletRequestWrapper extends HttpServletRequestWrapper { + + ServletRequestWrapper(HttpServletRequest request) { + super(request); + } + + @Override + public HttpSession getSession(boolean create) { + if (!create) { + return null; + } + throw notSupported(); + } + + @Override + public HttpSession getSession() { + throw notSupported(); + } + + private static UnsupportedOperationException notSupported() { + return new UnsupportedOperationException("Sessions are disabled so that web server is stateless"); + } + } +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/package-info.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/package-info.java new file mode 100644 index 00000000000..0ca88f82702 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/package-info.java @@ -0,0 +1,23 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +@ParametersAreNonnullByDefault +package org.sonar.server.platform.web; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/requestid/HttpRequestIdModule.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/requestid/HttpRequestIdModule.java new file mode 100644 index 00000000000..22fa06b2cc9 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/requestid/HttpRequestIdModule.java @@ -0,0 +1,31 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.web.requestid; + +import org.sonar.core.platform.Module; + +public class HttpRequestIdModule extends Module { + @Override + protected void configureModule() { + add(new RequestIdConfiguration(RequestIdGeneratorImpl.UUID_GENERATOR_RENEWAL_COUNT), + RequestIdGeneratorBaseImpl.class, + RequestIdGeneratorImpl.class); + } +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/requestid/RequestIdConfiguration.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/requestid/RequestIdConfiguration.java new file mode 100644 index 00000000000..4acf5961341 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/requestid/RequestIdConfiguration.java @@ -0,0 +1,35 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.web.requestid; + +public class RequestIdConfiguration { + /** + * @see RequestIdGeneratorImpl#mustRenewUuidGenerator(long) + */ + private final long uuidGeneratorRenewalCount; + + public RequestIdConfiguration(long uuidGeneratorRenewalCount) { + this.uuidGeneratorRenewalCount = uuidGeneratorRenewalCount; + } + + public long getUidGeneratorRenewalCount() { + return uuidGeneratorRenewalCount; + } +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/requestid/RequestIdGenerator.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/requestid/RequestIdGenerator.java new file mode 100644 index 00000000000..4296285dd14 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/requestid/RequestIdGenerator.java @@ -0,0 +1,30 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.web.requestid; + +/** + * Generate a Unique Identifier for Http Requests. + */ +public interface RequestIdGenerator { + /** + * Generate a new and unique request id for each call. + */ + String generate(); +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/requestid/RequestIdGeneratorBase.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/requestid/RequestIdGeneratorBase.java new file mode 100644 index 00000000000..c3781871a55 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/requestid/RequestIdGeneratorBase.java @@ -0,0 +1,29 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.web.requestid; + +import org.sonar.core.util.UuidGenerator; + +public interface RequestIdGeneratorBase { + /** + * Provides a new instance of {@link UuidGenerator.WithFixedBase} to be used by {@link RequestIdGeneratorImpl}. + */ + UuidGenerator.WithFixedBase createNew(); +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/requestid/RequestIdGeneratorBaseImpl.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/requestid/RequestIdGeneratorBaseImpl.java new file mode 100644 index 00000000000..30979665ea1 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/requestid/RequestIdGeneratorBaseImpl.java @@ -0,0 +1,31 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.web.requestid; + +import org.sonar.core.util.UuidGenerator; +import org.sonar.core.util.UuidGeneratorImpl; + +public class RequestIdGeneratorBaseImpl implements RequestIdGeneratorBase { + + @Override + public UuidGenerator.WithFixedBase createNew() { + return new UuidGeneratorImpl().withFixedBase(); + } +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/requestid/RequestIdGeneratorImpl.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/requestid/RequestIdGeneratorImpl.java new file mode 100644 index 00000000000..7010cac69de --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/requestid/RequestIdGeneratorImpl.java @@ -0,0 +1,96 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.web.requestid; + +import java.util.Base64; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import org.sonar.core.util.UuidGenerator; + +/** + * This implementation of {@link RequestIdGenerator} creates unique identifiers for HTTP requests leveraging + * {@link UuidGenerator.WithFixedBase#generate(int)} and a counter of HTTP requests. + * <p> + * To work around the limit of unique values produced by {@link UuidGenerator.WithFixedBase#generate(int)}, the + * {@link UuidGenerator.WithFixedBase} instance will be renewed every + * {@link RequestIdConfiguration#getUidGeneratorRenewalCount() RequestIdConfiguration#uidGeneratorRenewalCount} + * HTTP requests. + * </p> + * <p> + * This implementation is Thread safe. + * </p> + */ +public class RequestIdGeneratorImpl implements RequestIdGenerator { + /** + * The value to which the HTTP request count will be compared to (using a modulo operator, + * see {@link #mustRenewUuidGenerator(long)}). + * + * <p> + * This value can't be the last value before {@link UuidGenerator.WithFixedBase#generate(int)} returns a non unique + * value, ie. 2^23-1 because there is no guarantee the renewal will happen before any other thread calls + * {@link UuidGenerator.WithFixedBase#generate(int)} method of the deplated {@link UuidGenerator.WithFixedBase} instance. + * </p> + * + * <p> + * To keep a comfortable margin of error, 2^22 will be used. + * </p> + */ + public static final long UUID_GENERATOR_RENEWAL_COUNT = 4_194_304; + + private final AtomicLong counter = new AtomicLong(); + private final RequestIdGeneratorBase requestIdGeneratorBase; + private final RequestIdConfiguration requestIdConfiguration; + private final AtomicReference<UuidGenerator.WithFixedBase> uuidGenerator; + + public RequestIdGeneratorImpl(RequestIdGeneratorBase requestIdGeneratorBase, RequestIdConfiguration requestIdConfiguration) { + this.requestIdGeneratorBase = requestIdGeneratorBase; + this.uuidGenerator = new AtomicReference<>(requestIdGeneratorBase.createNew()); + this.requestIdConfiguration = requestIdConfiguration; + } + + @Override + public String generate() { + UuidGenerator.WithFixedBase currentUuidGenerator = this.uuidGenerator.get(); + long counterValue = counter.getAndIncrement(); + if (counterValue != 0 && mustRenewUuidGenerator(counterValue)) { + UuidGenerator.WithFixedBase newUuidGenerator = requestIdGeneratorBase.createNew(); + uuidGenerator.set(newUuidGenerator); + return generate(newUuidGenerator, counterValue); + } + return generate(currentUuidGenerator, counterValue); + } + + /** + * Since renewal of {@link UuidGenerator.WithFixedBase} instance is based on the HTTP request counter, only a single + * thread can get the right value which will make this method return true. So, this is thread-safe by design, therefor + * this method doesn't need external synchronization. + * <p> + * The value to which the counter is compared should however be chosen with caution: see {@link #UUID_GENERATOR_RENEWAL_COUNT}. + * </p> + */ + private boolean mustRenewUuidGenerator(long counter) { + return counter % requestIdConfiguration.getUidGeneratorRenewalCount() == 0; + } + + private static String generate(UuidGenerator.WithFixedBase uuidGenerator, long increment) { + return Base64.getEncoder().encodeToString(uuidGenerator.generate((int) increment)); + } + +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/requestid/RequestIdMDCStorage.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/requestid/RequestIdMDCStorage.java new file mode 100644 index 00000000000..53b4bf49e33 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/requestid/RequestIdMDCStorage.java @@ -0,0 +1,40 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.web.requestid; + +import org.slf4j.MDC; + +import static java.util.Objects.requireNonNull; + +/** + * Wraps MDC calls to store the HTTP request ID in the {@link MDC} into an {@link AutoCloseable}. + */ +public class RequestIdMDCStorage implements AutoCloseable { + public static final String HTTP_REQUEST_ID_MDC_KEY = "HTTP_REQUEST_ID"; + + public RequestIdMDCStorage(String requestId) { + MDC.put(HTTP_REQUEST_ID_MDC_KEY, requireNonNull(requestId, "Request ID can't be null")); + } + + @Override + public void close() { + MDC.remove(HTTP_REQUEST_ID_MDC_KEY); + } +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/requestid/package-info.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/requestid/package-info.java new file mode 100644 index 00000000000..413da7d0492 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/web/requestid/package-info.java @@ -0,0 +1,23 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +@ParametersAreNonnullByDefault +package org.sonar.server.platform.web.requestid; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/CachingRuleFinder.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/CachingRuleFinder.java new file mode 100644 index 00000000000..bd6995ab93b --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/CachingRuleFinder.java @@ -0,0 +1,197 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.rule; + +import com.google.common.collect.ImmutableListMultimap; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ListMultimap; +import com.google.common.collect.Ordering; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; +import org.sonar.api.rule.RuleKey; +import org.sonar.api.rule.RuleStatus; +import org.sonar.api.rules.Rule; +import org.sonar.api.rules.RuleFinder; +import org.sonar.api.rules.RulePriority; +import org.sonar.api.rules.RuleQuery; +import org.sonar.core.util.stream.MoreCollectors; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.rule.RuleDefinitionDto; +import org.sonar.db.rule.RuleDto; +import org.sonar.db.rule.RuleParamDto; +import org.sonar.markdown.Markdown; + +import static com.google.common.collect.Lists.newArrayList; +import static org.sonar.core.util.stream.MoreCollectors.toSet; +import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex; + +/** + * A {@link RuleFinder} implementation that retrieves all rule definitions and their parameter when instantiated, cache + * them in memory and provide implementation of {@link RuleFinder}'s method which only read from this data in memory. + */ +public class CachingRuleFinder implements RuleFinder { + + private static final Ordering<Map.Entry<RuleDefinitionDto, Rule>> FIND_BY_QUERY_ORDER = Ordering.natural().reverse().onResultOf(entry -> entry.getKey().getUpdatedAt()); + + private final Map<RuleDefinitionDto, Rule> rulesByRuleDefinition; + private final Map<Integer, Rule> rulesById; + private final Map<RuleKey, Rule> rulesByKey; + + public CachingRuleFinder(DbClient dbClient) { + try (DbSession dbSession = dbClient.openSession(false)) { + this.rulesByRuleDefinition = buildRulesByRuleDefinitionDto(dbClient, dbSession); + this.rulesById = this.rulesByRuleDefinition.entrySet().stream() + .collect(uniqueIndex(entry -> entry.getKey().getId(), Map.Entry::getValue)); + this.rulesByKey = this.rulesByRuleDefinition.entrySet().stream() + .collect(uniqueIndex(entry -> entry.getKey().getKey(), Map.Entry::getValue)); + } + } + + private static Map<RuleDefinitionDto, Rule> buildRulesByRuleDefinitionDto(DbClient dbClient, DbSession dbSession) { + List<RuleDefinitionDto> dtos = dbClient.ruleDao().selectAllDefinitions(dbSession); + Set<RuleKey> ruleKeys = dtos.stream().map(RuleDefinitionDto::getKey).collect(toSet(dtos.size())); + ListMultimap<Integer, RuleParamDto> ruleParamsByRuleId = retrieveRuleParameters(dbClient, dbSession, ruleKeys); + Map<RuleDefinitionDto, Rule> rulesByDefinition = new HashMap<>(dtos.size()); + for (RuleDefinitionDto definition : dtos) { + rulesByDefinition.put(definition, toRule(definition, ruleParamsByRuleId.get(definition.getId()))); + } + return ImmutableMap.copyOf(rulesByDefinition); + } + + private static ImmutableListMultimap<Integer, RuleParamDto> retrieveRuleParameters(DbClient dbClient, DbSession dbSession, Set<RuleKey> ruleKeys) { + if (ruleKeys.isEmpty()) { + return ImmutableListMultimap.of(); + } + return dbClient.ruleDao().selectRuleParamsByRuleKeys(dbSession, ruleKeys) + .stream() + .collect(MoreCollectors.index(RuleParamDto::getRuleId)); + } + + @Override + @Deprecated + @CheckForNull + public Rule findById(int ruleId) { + return rulesById.get(ruleId); + } + + @Override + @CheckForNull + public Rule findByKey(@Nullable String repositoryKey, @Nullable String key) { + if (repositoryKey == null || key == null) { + return null; + } + return findByKey(RuleKey.of(repositoryKey, key)); + } + + @Override + @CheckForNull + public Rule findByKey(RuleKey key) { + return rulesByKey.get(key); + } + + @Override + @CheckForNull + public Rule find(@Nullable RuleQuery query) { + if (query == null) { + return null; + } + + return rulesByRuleDefinition.entrySet().stream() + .filter(entry -> matchQuery(entry.getKey(), query)) + .sorted(FIND_BY_QUERY_ORDER) + .map(Map.Entry::getValue) + .findFirst() + .orElse(null); + } + + @Override + public Collection<Rule> findAll(@Nullable RuleQuery query) { + if (query == null) { + return Collections.emptyList(); + } + return rulesByRuleDefinition.entrySet().stream() + .filter(entry -> matchQuery(entry.getKey(), query)) + .sorted(FIND_BY_QUERY_ORDER) + .map(Map.Entry::getValue) + .collect(MoreCollectors.toList()); + } + + private static boolean matchQuery(RuleDefinitionDto ruleDefinition, RuleQuery ruleQuery) { + if (RuleStatus.REMOVED.equals(ruleDefinition.getStatus())) { + return false; + } + String repositoryKey = ruleQuery.getRepositoryKey(); + if (ruleQuery.getRepositoryKey() != null && !repositoryKey.equals(ruleDefinition.getRepositoryKey())) { + return false; + } + String key = ruleQuery.getKey(); + if (key != null && !key.equals(ruleDefinition.getRuleKey())) { + return false; + } + String configKey = ruleQuery.getConfigKey(); + return configKey == null || configKey.equals(ruleDefinition.getConfigKey()); + } + + private static Rule toRule(RuleDefinitionDto ruleDefinition, List<RuleParamDto> params) { + String severity = ruleDefinition.getSeverityString(); + String description = ruleDefinition.getDescription(); + RuleDto.Format descriptionFormat = ruleDefinition.getDescriptionFormat(); + + Rule apiRule = new Rule(); + apiRule + .setName(ruleDefinition.getName()) + .setLanguage(ruleDefinition.getLanguage()) + .setKey(ruleDefinition.getRuleKey()) + .setConfigKey(ruleDefinition.getConfigKey()) + .setIsTemplate(ruleDefinition.isTemplate()) + .setCreatedAt(new Date(ruleDefinition.getCreatedAt())) + .setUpdatedAt(new Date(ruleDefinition.getUpdatedAt())) + .setRepositoryKey(ruleDefinition.getRepositoryKey()) + .setSeverity(severity != null ? RulePriority.valueOf(severity) : null) + .setStatus(ruleDefinition.getStatus().name()) + .setSystemTags(ruleDefinition.getSystemTags().toArray(new String[ruleDefinition.getSystemTags().size()])) + .setTags(new String[0]) + .setId(ruleDefinition.getId()); + if (description != null && descriptionFormat != null) { + if (RuleDto.Format.HTML.equals(descriptionFormat)) { + apiRule.setDescription(description); + } else { + apiRule.setDescription(Markdown.convertToHtml(description)); + } + } + + List<org.sonar.api.rules.RuleParam> apiParams = newArrayList(); + for (RuleParamDto param : params) { + apiParams.add(new org.sonar.api.rules.RuleParam(apiRule, param.getName(), param.getDescription(), param.getType()) + .setDefaultValue(param.getDefaultValue())); + } + apiRule.setParams(apiParams); + + return apiRule; + } +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/CommonRuleDefinitions.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/CommonRuleDefinitions.java new file mode 100644 index 00000000000..252b0c1e332 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/CommonRuleDefinitions.java @@ -0,0 +1,26 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.rule; + +import org.sonar.api.server.rule.RulesDefinition; + +public interface CommonRuleDefinitions { + void define(RulesDefinition.Context context); +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/CommonRuleDefinitionsImpl.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/CommonRuleDefinitionsImpl.java new file mode 100644 index 00000000000..0a9c5407950 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/CommonRuleDefinitionsImpl.java @@ -0,0 +1,135 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.rule; + +import org.sonar.api.resources.Language; +import org.sonar.api.resources.Languages; +import org.sonar.api.rule.Severity; +import org.sonar.api.server.rule.RuleParamType; +import org.sonar.api.server.rule.RulesDefinition; + +import static org.sonar.server.rule.CommonRuleKeys.commonRepositoryForLang; + +/** + * Declare the few rules that are automatically created by core for all languages. These rules + * check measure values against thresholds defined in Quality profiles. + */ +// this class must not be mixed with other RulesDefinition so it does implement the interface RulesDefinitions. +// It replaces the common-rules that are still embedded within plugins. +public class CommonRuleDefinitionsImpl implements CommonRuleDefinitions { + + private final Languages languages; + + public CommonRuleDefinitionsImpl(Languages languages) { + this.languages = languages; + } + + @Override + public void define(RulesDefinition.Context context) { + for (Language language : languages.all()) { + RulesDefinition.NewRepository repo = context.createRepository(commonRepositoryForLang(language.getKey()), language.getKey()); + repo.setName("Common " + language.getName()); + defineBranchCoverageRule(repo); + defineLineCoverageRule(repo); + defineCommentDensityRule(repo); + defineDuplicatedBlocksRule(repo); + defineFailedUnitTestRule(repo); + defineSkippedUnitTestRule(repo); + repo.done(); + } + } + + private static void defineBranchCoverageRule(RulesDefinition.NewRepository repo) { + RulesDefinition.NewRule rule = repo.createRule(CommonRuleKeys.INSUFFICIENT_BRANCH_COVERAGE); + rule.setName("Branches should have sufficient coverage by tests") + .addTags("bad-practice") + .setHtmlDescription("An issue is created on a file as soon as the branch coverage on this file is less than the required threshold. " + + "It gives the number of branches to be covered in order to reach the required threshold.") + .setDebtRemediationFunction(rule.debtRemediationFunctions().linear("5min")) + .setGapDescription("number of uncovered conditions") + .setSeverity(Severity.MAJOR); + rule.createParam(CommonRuleKeys.INSUFFICIENT_BRANCH_COVERAGE_PROPERTY) + .setName("The minimum required branch coverage ratio") + .setDefaultValue("65") + .setType(RuleParamType.FLOAT); + } + + private static void defineLineCoverageRule(RulesDefinition.NewRepository repo) { + RulesDefinition.NewRule rule = repo.createRule(CommonRuleKeys.INSUFFICIENT_LINE_COVERAGE); + rule.setName("Lines should have sufficient coverage by tests") + .addTags("bad-practice") + .setHtmlDescription("An issue is created on a file as soon as the line coverage on this file is less than the required threshold. " + + "It gives the number of lines to be covered in order to reach the required threshold.") + .setDebtRemediationFunction(rule.debtRemediationFunctions().linear("2min")) + .setGapDescription("number of lines under the coverage threshold") + .setSeverity(Severity.MAJOR); + rule.createParam(CommonRuleKeys.INSUFFICIENT_LINE_COVERAGE_PROPERTY) + .setName("The minimum required line coverage ratio") + .setDefaultValue("65") + .setType(RuleParamType.FLOAT); + } + + private static void defineCommentDensityRule(RulesDefinition.NewRepository repo) { + RulesDefinition.NewRule rule = repo.createRule(CommonRuleKeys.INSUFFICIENT_COMMENT_DENSITY); + rule.setName("Source files should have a sufficient density of comment lines") + .addTags("convention") + .setHtmlDescription("An issue is created on a file as soon as the density of comment lines on this file is less than the required threshold. " + + "The number of comment lines to be written in order to reach the required threshold is provided by each issue message.") + .setDebtRemediationFunction(rule.debtRemediationFunctions().linear("2min")) + .setGapDescription("number of lines required to meet minimum density") + .setSeverity(Severity.MAJOR); + rule.createParam(CommonRuleKeys.INSUFFICIENT_COMMENT_DENSITY_PROPERTY) + .setName("The minimum required comment density") + .setDefaultValue("25") + .setType(RuleParamType.FLOAT); + } + + private static void defineDuplicatedBlocksRule(RulesDefinition.NewRepository repo) { + RulesDefinition.NewRule rule = repo.createRule(CommonRuleKeys.DUPLICATED_BLOCKS); + rule.setName("Source files should not have any duplicated blocks") + .addTags("pitfall") + .setHtmlDescription("An issue is created on a file as soon as there is at least one block of duplicated code on this file") + .setDebtRemediationFunction(rule.debtRemediationFunctions().linearWithOffset("10min", "10min")) + .setGapDescription("number of duplicate blocks") + .setSeverity(Severity.MAJOR); + } + + private static void defineFailedUnitTestRule(RulesDefinition.NewRepository repo) { + RulesDefinition.NewRule rule = repo.createRule(CommonRuleKeys.FAILED_UNIT_TESTS); + rule + .setName("Failed unit tests should be fixed") + .addTags("bug") + .setHtmlDescription( + "Test failures or errors generally indicate that regressions have been introduced. Those tests should be handled as soon as possible to reduce the cost to fix the corresponding regressions.") + .setDebtRemediationFunction(rule.debtRemediationFunctions().linear("10min")) + .setGapDescription("number of failed tests") + .setSeverity(Severity.MAJOR); + } + + private static void defineSkippedUnitTestRule(RulesDefinition.NewRepository repo) { + RulesDefinition.NewRule rule = repo.createRule(CommonRuleKeys.SKIPPED_UNIT_TESTS); + rule.setName("Skipped unit tests should be either removed or fixed") + .addTags("pitfall") + .setHtmlDescription("Skipped unit tests are considered as dead code. Either they should be activated again (and updated) or they should be removed.") + .setDebtRemediationFunction(rule.debtRemediationFunctions().linear("10min")) + .setGapDescription("number of skipped tests") + .setSeverity(Severity.MAJOR); + } +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/DeprecatedRulesDefinitionLoader.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/DeprecatedRulesDefinitionLoader.java new file mode 100644 index 00000000000..a6e255ed80f --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/DeprecatedRulesDefinitionLoader.java @@ -0,0 +1,216 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.rule; + +import com.google.common.base.Predicate; +import com.google.common.collect.Iterables; +import java.io.Reader; +import java.util.Collection; +import java.util.List; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.StringUtils; +import org.sonar.api.rule.RuleKey; +import org.sonar.api.rule.RuleStatus; +import org.sonar.api.rules.RuleParam; +import org.sonar.api.rules.RuleRepository; +import org.sonar.api.server.ServerSide; +import org.sonar.api.server.debt.DebtRemediationFunction; +import org.sonar.api.server.rule.RuleParamType; +import org.sonar.api.server.rule.RulesDefinition; +import org.sonar.api.utils.ValidationMessages; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; +import org.sonar.core.i18n.RuleI18nManager; +import org.sonar.server.debt.DebtModelPluginRepository; +import org.sonar.server.debt.DebtModelXMLExporter; +import org.sonar.server.debt.DebtModelXMLExporter.RuleDebt; +import org.sonar.server.debt.DebtRulesXMLImporter; +import org.sonar.server.plugins.ServerPluginRepository; + +import static com.google.common.collect.Lists.newArrayList; + +/** + * Inject deprecated RuleRepository into {@link org.sonar.api.server.rule.RulesDefinition} for backward-compatibility. + */ +@ServerSide +public class DeprecatedRulesDefinitionLoader { + + private static final Logger LOG = Loggers.get(DeprecatedRulesDefinitionLoader.class); + + private final RuleI18nManager i18n; + private final RuleRepository[] repositories; + + private final DebtModelPluginRepository languageModelFinder; + private final DebtRulesXMLImporter importer; + private final ServerPluginRepository serverPluginRepository; + + public DeprecatedRulesDefinitionLoader(RuleI18nManager i18n, DebtModelPluginRepository languageModelFinder, DebtRulesXMLImporter importer, + ServerPluginRepository serverPluginRepository, RuleRepository[] repositories) { + this.i18n = i18n; + this.serverPluginRepository = serverPluginRepository; + this.repositories = repositories; + this.languageModelFinder = languageModelFinder; + this.importer = importer; + } + + /** + * Used when no deprecated repositories + */ + public DeprecatedRulesDefinitionLoader(RuleI18nManager i18n, DebtModelPluginRepository languageModelFinder, DebtRulesXMLImporter importer, + ServerPluginRepository serverPluginRepository) { + this(i18n, languageModelFinder, importer, serverPluginRepository, new RuleRepository[0]); + } + + void complete(RulesDefinition.Context context) { + // Load rule debt definitions from xml files provided by plugin + List<RuleDebt> ruleDebts = loadRuleDebtList(); + + for (RuleRepository repository : repositories) { + context.setCurrentPluginKey(serverPluginRepository.getPluginKey(repository)); + // RuleRepository API does not handle difference between new and extended repositories, + RulesDefinition.NewRepository newRepository; + if (context.repository(repository.getKey()) == null) { + newRepository = context.createRepository(repository.getKey(), repository.getLanguage()); + newRepository.setName(repository.getName()); + } else { + newRepository = context.extendRepository(repository.getKey(), repository.getLanguage()); + } + for (org.sonar.api.rules.Rule rule : repository.createRules()) { + RulesDefinition.NewRule newRule = newRepository.createRule(rule.getKey()); + newRule.setName(ruleName(repository.getKey(), rule)); + newRule.setHtmlDescription(ruleDescription(repository.getKey(), rule)); + newRule.setInternalKey(rule.getConfigKey()); + newRule.setTemplate(rule.isTemplate()); + newRule.setSeverity(rule.getSeverity().toString()); + newRule.setStatus(rule.getStatus() == null ? RuleStatus.defaultStatus() : RuleStatus.valueOf(rule.getStatus())); + newRule.setTags(rule.getTags()); + for (RuleParam param : rule.getParams()) { + RulesDefinition.NewParam newParam = newRule.createParam(param.getKey()); + newParam.setDefaultValue(param.getDefaultValue()); + newParam.setDescription(paramDescription(repository.getKey(), rule.getKey(), param)); + newParam.setType(RuleParamType.parse(param.getType())); + } + updateRuleDebtDefinitions(newRule, repository.getKey(), rule.getKey(), ruleDebts); + } + newRepository.done(); + } + } + + private static void updateRuleDebtDefinitions(RulesDefinition.NewRule newRule, String repoKey, String ruleKey, List<RuleDebt> ruleDebts) { + RuleDebt ruleDebt = findRequirement(ruleDebts, repoKey, ruleKey); + if (ruleDebt != null) { + newRule.setDebtRemediationFunction(remediationFunction(DebtRemediationFunction.Type.valueOf(ruleDebt.function()), + ruleDebt.coefficient(), + ruleDebt.offset(), + newRule.debtRemediationFunctions(), + repoKey, ruleKey)); + } + } + + private static DebtRemediationFunction remediationFunction(DebtRemediationFunction.Type function, @Nullable String coefficient, @Nullable String offset, + RulesDefinition.DebtRemediationFunctions functions, String repoKey, String ruleKey) { + if (DebtRemediationFunction.Type.LINEAR.equals(function) && coefficient != null) { + return functions.linear(coefficient); + } else if (DebtRemediationFunction.Type.CONSTANT_ISSUE.equals(function) && offset != null) { + return functions.constantPerIssue(offset); + } else if (DebtRemediationFunction.Type.LINEAR_OFFSET.equals(function) && coefficient != null && offset != null) { + return functions.linearWithOffset(coefficient, offset); + } else { + throw new IllegalArgumentException(String.format("Debt definition on rule '%s:%s' is invalid", repoKey, ruleKey)); + } + } + + @CheckForNull + private String ruleName(String repositoryKey, org.sonar.api.rules.Rule rule) { + String name = i18n.getName(repositoryKey, rule.getKey()); + if (StringUtils.isNotBlank(name)) { + return name; + } + return StringUtils.defaultIfBlank(rule.getName(), null); + } + + @CheckForNull + private String ruleDescription(String repositoryKey, org.sonar.api.rules.Rule rule) { + String description = i18n.getDescription(repositoryKey, rule.getKey()); + if (StringUtils.isNotBlank(description)) { + return description; + } + return StringUtils.defaultIfBlank(rule.getDescription(), null); + } + + @CheckForNull + private String paramDescription(String repositoryKey, String ruleKey, RuleParam param) { + String desc = StringUtils.defaultIfEmpty( + i18n.getParamDescription(repositoryKey, ruleKey, param.getKey()), + param.getDescription()); + return StringUtils.defaultIfBlank(desc, null); + } + + public List<DebtModelXMLExporter.RuleDebt> loadRuleDebtList() { + List<RuleDebt> ruleDebtList = newArrayList(); + for (String pluginKey : getContributingPluginListWithoutSqale()) { + ruleDebtList.addAll(loadRuleDebtsFromXml(pluginKey)); + } + return ruleDebtList; + } + + public List<RuleDebt> loadRuleDebtsFromXml(String pluginKey) { + Reader xmlFileReader = null; + try { + xmlFileReader = languageModelFinder.createReaderForXMLFile(pluginKey); + ValidationMessages validationMessages = ValidationMessages.create(); + List<RuleDebt> rules = importer.importXML(xmlFileReader, validationMessages); + validationMessages.log(LOG); + return rules; + } finally { + IOUtils.closeQuietly(xmlFileReader); + } + } + + private Collection<String> getContributingPluginListWithoutSqale() { + Collection<String> pluginList = newArrayList(languageModelFinder.getContributingPluginList()); + pluginList.remove(DebtModelPluginRepository.DEFAULT_MODEL); + return pluginList; + } + + @CheckForNull + private static RuleDebt findRequirement(List<RuleDebt> requirements, final String repoKey, final String ruleKey) { + return Iterables.find(requirements, new RuleDebtMatchRepoKeyAndRuleKey(repoKey, ruleKey), null); + } + + private static class RuleDebtMatchRepoKeyAndRuleKey implements Predicate<RuleDebt> { + + private final String repoKey; + private final String ruleKey; + + public RuleDebtMatchRepoKeyAndRuleKey(String repoKey, String ruleKey) { + this.repoKey = repoKey; + this.ruleKey = ruleKey; + } + + @Override + public boolean apply(@Nonnull RuleDebt input) { + return input.ruleKey().equals(RuleKey.of(repoKey, ruleKey)); + } + } +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/Rule.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/Rule.java new file mode 100644 index 00000000000..5d6a2a32339 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/Rule.java @@ -0,0 +1,110 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.rule; + +import org.sonar.api.rule.RuleKey; +import org.sonar.api.rule.RuleStatus; +import org.sonar.api.server.debt.DebtRemediationFunction; + +import javax.annotation.CheckForNull; + +import java.util.Date; +import java.util.List; + +/** + * @since 4.4 + */ +public interface Rule { + + RuleKey key(); + + String language(); + + String name(); + + @CheckForNull + String htmlDescription(); + + @CheckForNull + String markdownDescription(); + + String effortToFixDescription(); + + /** + * Default severity when activated on a Quality profile + * + * @see org.sonar.api.rule.Severity + */ + String severity(); + + /** + * @see org.sonar.api.rule.RuleStatus + */ + RuleStatus status(); + + boolean isTemplate(); + + @CheckForNull + RuleKey templateKey(); + + /** + * Tags that can be customized by administrators + */ + List<String> tags(); + + /** + * Read-only tags defined by plugins + */ + List<String> systemTags(); + + List<RuleParam> params(); + + @CheckForNull + RuleParam param(final String key); + + boolean debtOverloaded(); + + @CheckForNull + DebtRemediationFunction debtRemediationFunction(); + + @CheckForNull + DebtRemediationFunction defaultDebtRemediationFunction(); + + Date createdAt(); + + Date updatedAt(); + + @CheckForNull + String internalKey(); + + @CheckForNull + String markdownNote(); + + @CheckForNull + String noteLogin(); + + @CheckForNull + Date noteCreatedAt(); + + @CheckForNull + Date noteUpdatedAt(); + + boolean isManual(); +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/RuleDefinitionsLoader.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/RuleDefinitionsLoader.java new file mode 100644 index 00000000000..80da97d352f --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/RuleDefinitionsLoader.java @@ -0,0 +1,64 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.rule; + +import org.sonar.api.server.rule.RulesDefinition; +import org.sonar.api.impl.server.RulesDefinitionContext; +import org.sonar.server.plugins.ServerPluginRepository; + +/** + * Loads all instances of {@link RulesDefinition}. Used during server startup + * and restore of debt model backup. + */ +public class RuleDefinitionsLoader { + + private final DeprecatedRulesDefinitionLoader deprecatedDefConverter; + private final CommonRuleDefinitions coreCommonDefs; + private final RulesDefinition[] pluginDefs; + private final ServerPluginRepository serverPluginRepository; + + public RuleDefinitionsLoader(DeprecatedRulesDefinitionLoader deprecatedDefConverter, + CommonRuleDefinitions coreCommonDefs, ServerPluginRepository serverPluginRepository, RulesDefinition[] pluginDefs) { + this.deprecatedDefConverter = deprecatedDefConverter; + this.coreCommonDefs = coreCommonDefs; + this.serverPluginRepository = serverPluginRepository; + this.pluginDefs = pluginDefs; + } + + /** + * Used when no definitions at all. + */ + public RuleDefinitionsLoader(DeprecatedRulesDefinitionLoader converter, + CommonRuleDefinitions coreCommonDefs, ServerPluginRepository serverPluginRepository) { + this(converter, coreCommonDefs, serverPluginRepository, new RulesDefinition[0]); + } + + public RulesDefinition.Context load() { + RulesDefinition.Context context = new RulesDefinitionContext(); + for (RulesDefinition pluginDefinition : pluginDefs) { + context.setCurrentPluginKey(serverPluginRepository.getPluginKey(pluginDefinition)); + pluginDefinition.define(context); + } + deprecatedDefConverter.complete(context); + context.setCurrentPluginKey(null); + coreCommonDefs.define(context); + return context; + } +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/RuleParam.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/RuleParam.java new file mode 100644 index 00000000000..0a65abd961b --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/RuleParam.java @@ -0,0 +1,37 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.rule; + +import org.sonar.api.server.rule.RuleParamType; + +import javax.annotation.CheckForNull; + +public interface RuleParam { + + String key(); + + @CheckForNull + String description(); + + @CheckForNull + String defaultValue(); + + RuleParamType type(); +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/SingleDeprecatedRuleKey.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/SingleDeprecatedRuleKey.java new file mode 100644 index 00000000000..b96d906bf71 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/SingleDeprecatedRuleKey.java @@ -0,0 +1,154 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.rule; + +import java.util.Objects; +import java.util.Set; +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; +import org.sonar.api.rule.RuleKey; +import org.sonar.api.server.rule.RulesDefinition; +import org.sonar.core.util.stream.MoreCollectors; +import org.sonar.db.rule.DeprecatedRuleKeyDto; + +@Immutable +class SingleDeprecatedRuleKey { + private String oldRuleKey; + private String oldRepositoryKey; + private String newRuleKey; + private String newRepositoryKey; + private String uuid; + private Integer ruleId; + + /** + * static methods {@link #from(RulesDefinition.Rule)} and {@link #from(DeprecatedRuleKeyDto)} must be used + */ + private SingleDeprecatedRuleKey() { + // empty + } + + public static Set<SingleDeprecatedRuleKey> from(RulesDefinition.Rule rule) { + return rule.deprecatedRuleKeys().stream() + .map(r -> new SingleDeprecatedRuleKey() + .setNewRepositoryKey(rule.repository().key()) + .setNewRuleKey(rule.key()) + .setOldRepositoryKey(r.repository()) + .setOldRuleKey(r.rule())) + .collect(MoreCollectors.toSet(rule.deprecatedRuleKeys().size())); + } + + public static SingleDeprecatedRuleKey from(DeprecatedRuleKeyDto rule) { + return new SingleDeprecatedRuleKey() + .setUuid(rule.getUuid()) + .setRuleId(rule.getRuleId()) + .setNewRepositoryKey(rule.getNewRepositoryKey()) + .setNewRuleKey(rule.getNewRuleKey()) + .setOldRepositoryKey(rule.getOldRepositoryKey()) + .setOldRuleKey(rule.getOldRuleKey()); + } + + public String getOldRuleKey() { + return oldRuleKey; + } + + public String getOldRepositoryKey() { + return oldRepositoryKey; + } + + public RuleKey getOldRuleKeyAsRuleKey() { + return RuleKey.of(oldRepositoryKey, oldRuleKey); + } + + + public RuleKey getNewRuleKeyAsRuleKey() { + return RuleKey.of(newRepositoryKey, newRuleKey); + } + + @CheckForNull + public String getNewRuleKey() { + return newRuleKey; + } + + @CheckForNull + public String getNewRepositoryKey() { + return newRepositoryKey; + } + + @CheckForNull + public String getUuid() { + return uuid; + } + + @CheckForNull + public Integer getRuleId() { + return ruleId; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof SingleDeprecatedRuleKey)) { + return false; + } + SingleDeprecatedRuleKey that = (SingleDeprecatedRuleKey) o; + return Objects.equals(oldRuleKey, that.oldRuleKey) && + Objects.equals(oldRepositoryKey, that.oldRepositoryKey) && + Objects.equals(newRuleKey, that.newRuleKey) && + Objects.equals(newRepositoryKey, that.newRepositoryKey); + } + + @Override + public int hashCode() { + return Objects.hash(oldRuleKey, oldRepositoryKey, newRuleKey, newRepositoryKey); + } + + private SingleDeprecatedRuleKey setRuleId(Integer ruleId) { + this.ruleId = ruleId; + return this; + } + + private SingleDeprecatedRuleKey setUuid(String uuid) { + this.uuid = uuid; + return this; + } + + private SingleDeprecatedRuleKey setOldRuleKey(String oldRuleKey) { + this.oldRuleKey = oldRuleKey; + return this; + } + + private SingleDeprecatedRuleKey setOldRepositoryKey(String oldRepositoryKey) { + this.oldRepositoryKey = oldRepositoryKey; + return this; + } + + private SingleDeprecatedRuleKey setNewRuleKey(@Nullable String newRuleKey) { + this.newRuleKey = newRuleKey; + return this; + } + + private SingleDeprecatedRuleKey setNewRepositoryKey(@Nullable String newRepositoryKey) { + this.newRepositoryKey = newRepositoryKey; + return this; + } +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/WebServerRuleFinder.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/WebServerRuleFinder.java new file mode 100644 index 00000000000..aadbb34d40b --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/WebServerRuleFinder.java @@ -0,0 +1,41 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.rule; + +import org.sonar.api.rules.RuleFinder; + +/** + * {@link RuleFinder} implementation that supports caching used by the Web Server. + * <p> + * Caching is enabled right after loading of rules is done (see {@link RegisterRules}) and disabled + * once all startup tasks are done (see {@link org.sonar.server.platform.platformlevel.PlatformLevelStartup}). + * </p> + */ +public interface WebServerRuleFinder extends RuleFinder { + /** + * Enable caching. + */ + void startCaching(); + + /** + * Disable caching. + */ + void stopCaching(); +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/WebServerRuleFinderImpl.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/WebServerRuleFinderImpl.java new file mode 100644 index 00000000000..db347d46132 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/WebServerRuleFinderImpl.java @@ -0,0 +1,84 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.rule; + +import com.google.common.annotations.VisibleForTesting; +import java.util.Collection; +import javax.annotation.CheckForNull; +import org.sonar.api.rule.RuleKey; +import org.sonar.api.rules.Rule; +import org.sonar.api.rules.RuleFinder; +import org.sonar.api.rules.RuleQuery; +import org.sonar.db.DbClient; +import org.sonar.server.organization.DefaultOrganizationProvider; + +public class WebServerRuleFinderImpl implements WebServerRuleFinder { + private final DbClient dbClient; + private final RuleFinder defaultFinder; + @VisibleForTesting + RuleFinder delegate; + + public WebServerRuleFinderImpl(DbClient dbClient, DefaultOrganizationProvider defaultOrganizationProvider) { + this.dbClient = dbClient; + this.defaultFinder = new DefaultRuleFinder(dbClient, defaultOrganizationProvider); + this.delegate = this.defaultFinder; + } + + @Override + public void startCaching() { + this.delegate = new CachingRuleFinder(dbClient); + } + + @Override + public void stopCaching() { + this.delegate = this.defaultFinder; + } + + @Override + @CheckForNull + @Deprecated + public Rule findById(int ruleId) { + return delegate.findById(ruleId); + } + + @Override + @CheckForNull + public Rule findByKey(String repositoryKey, String key) { + return delegate.findByKey(repositoryKey, key); + } + + @Override + @CheckForNull + public Rule findByKey(RuleKey key) { + return delegate.findByKey(key); + } + + @Override + @CheckForNull + public Rule find(RuleQuery query) { + return delegate.find(query); + } + + @Override + public Collection<Rule> findAll(RuleQuery query) { + return delegate.findAll(query); + } + +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/package-info.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/package-info.java new file mode 100644 index 00000000000..09d28ba5169 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/rule/package-info.java @@ -0,0 +1,22 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +@ParametersAreNonnullByDefault package org.sonar.server.rule; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/GeneratePluginIndex.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/GeneratePluginIndex.java new file mode 100644 index 00000000000..22e5bf4257e --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/GeneratePluginIndex.java @@ -0,0 +1,97 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.startup; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.nio.charset.StandardCharsets; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang.CharUtils; +import org.sonar.api.Startable; +import org.sonar.api.server.ServerSide; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; +import org.sonar.api.utils.log.Profiler; +import org.sonar.server.platform.ServerFileSystem; +import org.sonar.server.plugins.InstalledPlugin; +import org.sonar.server.plugins.PluginFileSystem; + +/** + * The file deploy/plugins/index.txt is required for old versions of SonarLint. + * They don't use the web service api/plugins/installed to get the list + * of installed plugins. + * https://jira.sonarsource.com/browse/SLCORE-146 + */ +@ServerSide +public final class GeneratePluginIndex implements Startable { + + private static final Logger LOG = Loggers.get(GeneratePluginIndex.class); + + private final ServerFileSystem serverFs; + private final PluginFileSystem pluginFs; + + public GeneratePluginIndex(ServerFileSystem serverFs, PluginFileSystem pluginFs) { + this.serverFs = serverFs; + this.pluginFs = pluginFs; + } + + @Override + public void start() { + Profiler profiler = Profiler.create(LOG).startInfo("Generate scanner plugin index"); + writeIndex(serverFs.getPluginIndex()); + profiler.stopDebug(); + } + + @Override + public void stop() { + // Nothing to do + } + + private void writeIndex(File indexFile) { + try { + FileUtils.forceMkdir(indexFile.getParentFile()); + try (Writer writer = new OutputStreamWriter(new FileOutputStream(indexFile), StandardCharsets.UTF_8)) { + for (InstalledPlugin plugin : pluginFs.getInstalledFiles()) { + writer.append(toRow(plugin)); + writer.append(CharUtils.LF); + } + writer.flush(); + } + } catch (IOException e) { + throw new IllegalStateException("Unable to generate plugin index at " + indexFile, e); + } + } + + private static String toRow(InstalledPlugin file) { + StringBuilder sb = new StringBuilder(); + sb.append(file.getPluginInfo().getKey()) + .append(",") + .append(file.getPluginInfo().isSonarLintSupported()) + .append(",") + .append(file.getLoadedJar().getFile().getName()) + .append("|") + .append(file.getLoadedJar().getMd5()); + return sb.toString(); + } + +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/LogServerId.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/LogServerId.java new file mode 100644 index 00000000000..5091d13b22a --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/LogServerId.java @@ -0,0 +1,44 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.startup; + +import org.picocontainer.Startable; +import org.sonar.api.platform.Server; +import org.sonar.api.utils.log.Loggers; + +public final class LogServerId implements Startable { + + private final Server server; + + public LogServerId(Server server) { + this.server = server; + } + + @Override + public void start() { + Loggers.get(getClass()).info("Server ID: " + server.getId()); + } + + @Override + public void stop() { + // nothing to do + } + +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/RegisterMetrics.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/RegisterMetrics.java new file mode 100644 index 00000000000..615d91cef80 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/RegisterMetrics.java @@ -0,0 +1,139 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.startup; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.Maps; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.picocontainer.Startable; +import org.sonar.api.measures.CoreMetrics; +import org.sonar.api.measures.Metric; +import org.sonar.api.measures.Metrics; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; +import org.sonar.api.utils.log.Profiler; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.metric.MetricDto; +import org.sonar.server.metric.MetricToDto; + +import static com.google.common.collect.FluentIterable.from; +import static com.google.common.collect.Iterables.concat; +import static com.google.common.collect.Lists.newArrayList; + +public class RegisterMetrics implements Startable { + + private static final Logger LOG = Loggers.get(RegisterMetrics.class); + + private final DbClient dbClient; + private final Metrics[] metricsRepositories; + + public RegisterMetrics(DbClient dbClient, Metrics[] metricsRepositories) { + this.dbClient = dbClient; + this.metricsRepositories = metricsRepositories; + } + + /** + * Used when no plugin is defining Metrics + */ + public RegisterMetrics(DbClient dbClient) { + this(dbClient, new Metrics[] {}); + } + + @Override + public void start() { + register(concat(CoreMetrics.getMetrics(), getPluginMetrics())); + } + + @Override + public void stop() { + // nothing to do + } + + void register(Iterable<Metric> metrics) { + Profiler profiler = Profiler.create(LOG).startInfo("Register metrics"); + try (DbSession session = dbClient.openSession(false)) { + save(session, metrics); + sanitizeQualityGates(session); + session.commit(); + } + profiler.stopDebug(); + } + + private void sanitizeQualityGates(DbSession session) { + dbClient.gateConditionDao().deleteConditionsWithInvalidMetrics(session); + } + + private void save(DbSession session, Iterable<Metric> metrics) { + Map<String, MetricDto> basesByKey = new HashMap<>(); + for (MetricDto base : from(dbClient.metricDao().selectAll(session)).toList()) { + basesByKey.put(base.getKey(), base); + } + + for (Metric metric : metrics) { + MetricDto dto = MetricToDto.INSTANCE.apply(metric); + MetricDto base = basesByKey.get(metric.getKey()); + if (base == null) { + // new metric, never installed + dbClient.metricDao().insert(session, dto); + } else if (!base.isUserManaged()) { + // existing metric, update changes. Existing custom metrics are kept without applying changes. + dto.setId(base.getId()); + dbClient.metricDao().update(session, dto); + } + basesByKey.remove(metric.getKey()); + } + + for (MetricDto nonUpdatedBase : basesByKey.values()) { + if (!nonUpdatedBase.isUserManaged() && dbClient.metricDao().disableCustomByKey(session, nonUpdatedBase.getKey())) { + LOG.info("Disable metric {} [{}]", nonUpdatedBase.getShortName(), nonUpdatedBase.getKey()); + } + } + } + + @VisibleForTesting + List<Metric> getPluginMetrics() { + List<Metric> metricsToRegister = newArrayList(); + Map<String, Metrics> metricsByRepository = Maps.newHashMap(); + for (Metrics metrics : metricsRepositories) { + checkMetrics(metricsByRepository, metrics); + metricsToRegister.addAll(metrics.getMetrics()); + } + + return metricsToRegister; + } + + private void checkMetrics(Map<String, Metrics> metricsByRepository, Metrics metrics) { + for (Metric metric : metrics.getMetrics()) { + String metricKey = metric.getKey(); + if (CoreMetrics.getMetrics().contains(metric)) { + throw new IllegalStateException(String.format("Metric [%s] is already defined by SonarQube", metricKey)); + } + Metrics anotherRepository = metricsByRepository.get(metricKey); + if (anotherRepository != null) { + throw new IllegalStateException(String.format("Metric [%s] is already defined by the repository [%s]", metricKey, anotherRepository)); + } + metricsByRepository.put(metricKey, metrics); + } + } + +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/RegisterPermissionTemplates.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/RegisterPermissionTemplates.java new file mode 100644 index 00000000000..fd2ff4590cf --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/RegisterPermissionTemplates.java @@ -0,0 +1,128 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.startup; + +import org.picocontainer.Startable; +import org.sonar.api.security.DefaultGroups; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; +import org.sonar.api.utils.log.Profiler; +import org.sonar.api.web.UserRole; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.organization.DefaultTemplates; +import org.sonar.db.permission.OrganizationPermission; +import org.sonar.db.permission.template.PermissionTemplateDto; +import org.sonar.db.user.GroupDto; +import org.sonar.server.organization.DefaultOrganizationProvider; + +import java.util.Date; +import java.util.Optional; + +import static java.lang.String.format; + +public class RegisterPermissionTemplates implements Startable { + + private static final Logger LOG = Loggers.get(RegisterPermissionTemplates.class); + private static final String DEFAULT_TEMPLATE_UUID = "default_template"; + + private final DbClient dbClient; + private final DefaultOrganizationProvider defaultOrganizationProvider; + + public RegisterPermissionTemplates(DbClient dbClient, DefaultOrganizationProvider defaultOrganizationProvider) { + this.dbClient = dbClient; + this.defaultOrganizationProvider = defaultOrganizationProvider; + } + + @Override + public void start() { + Profiler profiler = Profiler.create(Loggers.get(getClass())).startInfo("Register permission templates"); + + try (DbSession dbSession = dbClient.openSession(false)) { + String defaultOrganizationUuid = defaultOrganizationProvider.get().getUuid(); + Optional<DefaultTemplates> defaultTemplates = dbClient.organizationDao().getDefaultTemplates(dbSession, defaultOrganizationUuid); + if (!defaultTemplates.isPresent()) { + PermissionTemplateDto defaultTemplate = getOrInsertDefaultTemplate(dbSession, defaultOrganizationUuid); + dbClient.organizationDao().setDefaultTemplates(dbSession, defaultOrganizationUuid, new DefaultTemplates().setProjectUuid(defaultTemplate.getUuid())); + dbSession.commit(); + } + } + + profiler.stopDebug(); + } + + @Override + public void stop() { + // nothing to do + } + + private PermissionTemplateDto getOrInsertDefaultTemplate(DbSession dbSession, String defaultOrganizationUuid) { + PermissionTemplateDto permissionTemplateDto = dbClient.permissionTemplateDao().selectByUuid(dbSession, DEFAULT_TEMPLATE_UUID); + if (permissionTemplateDto != null) { + return permissionTemplateDto; + } + + PermissionTemplateDto template = new PermissionTemplateDto() + .setOrganizationUuid(defaultOrganizationUuid) + .setName("Default template") + .setUuid(DEFAULT_TEMPLATE_UUID) + .setDescription("This permission template will be used as default when no other permission configuration is available") + .setCreatedAt(new Date()) + .setUpdatedAt(new Date()); + + dbClient.permissionTemplateDao().insert(dbSession, template); + insertDefaultGroupPermissions(dbSession, template); + dbSession.commit(); + return template; + } + + private void insertDefaultGroupPermissions(DbSession dbSession, PermissionTemplateDto template) { + insertPermissionForAdministrators(dbSession, template); + insertPermissionsForDefaultGroup(dbSession, template); + } + + private void insertPermissionForAdministrators(DbSession dbSession, PermissionTemplateDto template) { + Optional<GroupDto> admins = dbClient.groupDao().selectByName(dbSession, template.getOrganizationUuid(), DefaultGroups.ADMINISTRATORS); + if (admins.isPresent()) { + insertGroupPermission(dbSession, template, UserRole.ADMIN, admins.get()); + insertGroupPermission(dbSession, template, OrganizationPermission.APPLICATION_CREATOR.getKey(), admins.get()); + insertGroupPermission(dbSession, template, OrganizationPermission.PORTFOLIO_CREATOR.getKey(), admins.get()); + } else { + LOG.error("Cannot setup default permission for group: " + DefaultGroups.ADMINISTRATORS); + } + } + + private void insertPermissionsForDefaultGroup(DbSession dbSession, PermissionTemplateDto template) { + String organizationUuid = template.getOrganizationUuid(); + Integer defaultGroupId = dbClient.organizationDao().getDefaultGroupId(dbSession, organizationUuid) + .orElseThrow(() -> new IllegalStateException(format("Default group for organization %s is not defined", organizationUuid))); + GroupDto defaultGroup = Optional.ofNullable(dbClient.groupDao().selectById(dbSession, defaultGroupId)) + .orElseThrow(() -> new IllegalStateException(format("Default group with id %s for organization %s doesn't exist", defaultGroupId, organizationUuid))); + insertGroupPermission(dbSession, template, UserRole.USER, defaultGroup); + insertGroupPermission(dbSession, template, UserRole.CODEVIEWER, defaultGroup); + insertGroupPermission(dbSession, template, UserRole.ISSUE_ADMIN, defaultGroup); + insertGroupPermission(dbSession, template, UserRole.SECURITYHOTSPOT_ADMIN, defaultGroup); + } + + private void insertGroupPermission(DbSession dbSession, PermissionTemplateDto template, String permission, GroupDto group) { + dbClient.permissionTemplateDao().insertGroupPermission(dbSession, template.getId(), group.getId(), permission); + } + +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/RegisterPlugins.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/RegisterPlugins.java new file mode 100644 index 00000000000..cefa3fbb571 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/RegisterPlugins.java @@ -0,0 +1,104 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.startup; + +import java.util.Map; +import java.util.stream.Collectors; +import org.sonar.api.Startable; +import org.sonar.api.server.ServerSide; +import org.sonar.api.utils.System2; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; +import org.sonar.api.utils.log.Profiler; +import org.sonar.core.platform.PluginInfo; +import org.sonar.core.util.UuidFactory; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.plugin.PluginDto; +import org.sonar.server.plugins.InstalledPlugin; +import org.sonar.server.plugins.PluginFileSystem; + +import static java.util.function.Function.identity; + +/** + * Take care to update the 'plugins' table at startup. + */ +@ServerSide +public class RegisterPlugins implements Startable { + + private static final Logger LOG = Loggers.get(RegisterPlugins.class); + + private final PluginFileSystem pluginFileSystem; + private final DbClient dbClient; + private final UuidFactory uuidFactory; + private final System2 system; + + public RegisterPlugins(PluginFileSystem pluginFileSystem, DbClient dbClient, UuidFactory uuidFactory, System2 system) { + this.pluginFileSystem = pluginFileSystem; + this.dbClient = dbClient; + this.uuidFactory = uuidFactory; + this.system = system; + } + + @Override + public void start() { + Profiler profiler = Profiler.create(LOG).startInfo("Register plugins"); + updateDB(); + profiler.stopDebug(); + } + + @Override + public void stop() { + // Nothing to do + } + + private void updateDB() { + long now = system.now(); + try (DbSession dbSession = dbClient.openSession(false)) { + Map<String, PluginDto> allPreviousPluginsByKey = dbClient.pluginDao().selectAll(dbSession).stream() + .collect(Collectors.toMap(PluginDto::getKee, identity())); + for (InstalledPlugin installed : pluginFileSystem.getInstalledFiles()) { + PluginInfo info = installed.getPluginInfo(); + PluginDto previousDto = allPreviousPluginsByKey.get(info.getKey()); + if (previousDto == null) { + LOG.debug("Register new plugin {}", info.getKey()); + PluginDto pluginDto = new PluginDto() + .setUuid(uuidFactory.create()) + .setKee(info.getKey()) + .setBasePluginKey(info.getBasePlugin()) + .setFileHash(installed.getLoadedJar().getMd5()) + .setCreatedAt(now) + .setUpdatedAt(now); + dbClient.pluginDao().insert(dbSession, pluginDto); + } else if (!previousDto.getFileHash().equals(installed.getLoadedJar().getMd5())) { + LOG.debug("Update plugin {}", info.getKey()); + previousDto + .setBasePluginKey(info.getBasePlugin()) + .setFileHash(installed.getLoadedJar().getMd5()) + .setUpdatedAt(now); + dbClient.pluginDao().update(dbSession, previousDto); + } + // Don't remove uninstalled plugins, because corresponding rules and active rules are also not deleted + } + dbSession.commit(); + } + } + +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/RenameDeprecatedPropertyKeys.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/RenameDeprecatedPropertyKeys.java new file mode 100644 index 00000000000..7be27df5c8a --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/RenameDeprecatedPropertyKeys.java @@ -0,0 +1,56 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.startup; + +import com.google.common.base.Strings; +import org.picocontainer.Startable; +import org.sonar.api.config.PropertyDefinition; +import org.sonar.api.config.PropertyDefinitions; +import org.sonar.api.utils.log.Loggers; +import org.sonar.db.property.PropertiesDao; + +/** + * @since 3.4 + */ +public class RenameDeprecatedPropertyKeys implements Startable { + + private PropertiesDao dao; + private PropertyDefinitions definitions; + + public RenameDeprecatedPropertyKeys(PropertiesDao dao, PropertyDefinitions definitions) { + this.dao = dao; + this.definitions = definitions; + } + + @Override + public void start() { + Loggers.get(RenameDeprecatedPropertyKeys.class).info("Rename deprecated property keys"); + for (PropertyDefinition definition : definitions.getAll()) { + if (!Strings.isNullOrEmpty(definition.deprecatedKey())) { + dao.renamePropertyKey(definition.deprecatedKey(), definition.key()); + } + } + } + + @Override + public void stop() { + // nothing to do + } +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/package-info.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/package-info.java new file mode 100644 index 00000000000..c43f2781b47 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/package-info.java @@ -0,0 +1,23 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +@ParametersAreNonnullByDefault +package org.sonar.server.startup; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/telemetry/TelemetryClient.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/telemetry/TelemetryClient.java new file mode 100644 index 00000000000..719405aee62 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/telemetry/TelemetryClient.java @@ -0,0 +1,92 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.telemetry; + +import java.io.IOException; +import okhttp3.Call; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import org.sonar.api.Startable; +import org.sonar.api.config.Configuration; +import org.sonar.api.server.ServerSide; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; + +import static org.sonar.process.ProcessProperties.Property.SONAR_TELEMETRY_URL; + +@ServerSide +public class TelemetryClient implements Startable { + private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); + private static final Logger LOG = Loggers.get(TelemetryClient.class); + + private final OkHttpClient okHttpClient; + private final Configuration config; + private String serverUrl; + + public TelemetryClient(OkHttpClient okHttpClient, Configuration config) { + this.okHttpClient = okHttpClient; + this.config = config; + } + + void upload(String json) throws IOException { + Request request = buildHttpRequest(json); + execute(okHttpClient.newCall(request)); + } + + void optOut(String json) { + Request.Builder request = new Request.Builder(); + request.url(serverUrl); + RequestBody body = RequestBody.create(JSON, json); + request.delete(body); + + try { + execute(okHttpClient.newCall(request.build())); + } catch (IOException e) { + LOG.debug("Error when sending opt-out usage statistics: {}", e.getMessage()); + } + } + + private Request buildHttpRequest(String json) { + Request.Builder request = new Request.Builder(); + request.url(serverUrl); + RequestBody body = RequestBody.create(JSON, json); + request.post(body); + return request.build(); + } + + private static void execute(Call call) throws IOException { + try (Response ignored = call.execute()) { + // auto close connection to avoid leaked connection + } + } + + @Override + public void start() { + this.serverUrl = config.get(SONAR_TELEMETRY_URL.getKey()).orElseThrow(() -> new IllegalStateException(String.format("Setting '%s' must be provided.", SONAR_TELEMETRY_URL))); + } + + @Override + public void stop() { + // Nothing to do + } +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/telemetry/TelemetryDaemon.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/telemetry/TelemetryDaemon.java new file mode 100644 index 00000000000..64f44c3cc88 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/telemetry/TelemetryDaemon.java @@ -0,0 +1,175 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.telemetry; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import java.io.IOException; +import java.io.StringWriter; +import java.util.Date; +import java.util.Optional; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import org.picocontainer.Startable; +import org.sonar.api.config.Configuration; +import org.sonar.api.server.ServerSide; +import org.sonar.api.utils.System2; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; +import org.sonar.api.utils.text.JsonWriter; +import org.sonar.server.property.InternalProperties; +import org.sonar.server.util.GlobalLockManager; + +import static org.sonar.api.utils.DateUtils.formatDate; +import static org.sonar.api.utils.DateUtils.parseDate; +import static org.sonar.process.ProcessProperties.Property.SONAR_TELEMETRY_ENABLE; +import static org.sonar.process.ProcessProperties.Property.SONAR_TELEMETRY_FREQUENCY_IN_SECONDS; +import static org.sonar.process.ProcessProperties.Property.SONAR_TELEMETRY_URL; +import static org.sonar.server.telemetry.TelemetryDataJsonWriter.writeTelemetryData; + +@ServerSide +public class TelemetryDaemon implements Startable { + private static final String THREAD_NAME_PREFIX = "sq-telemetry-service-"; + private static final int SEVEN_DAYS = 7 * 24 * 60 * 60 * 1_000; + private static final String I_PROP_LAST_PING = "telemetry.lastPing"; + private static final String I_PROP_OPT_OUT = "telemetry.optOut"; + private static final String LOCK_NAME = "TelemetryStat"; + private static final Logger LOG = Loggers.get(TelemetryDaemon.class); + private static final String LOCK_DELAY_SEC = "sonar.telemetry.lock.delay"; + + private final TelemetryDataLoader dataLoader; + private final TelemetryClient telemetryClient; + private final GlobalLockManager lockManager; + private final Configuration config; + private final InternalProperties internalProperties; + private final System2 system2; + + private ScheduledExecutorService executorService; + + public TelemetryDaemon(TelemetryDataLoader dataLoader, TelemetryClient telemetryClient, Configuration config, + InternalProperties internalProperties, GlobalLockManager lockManager, System2 system2) { + this.dataLoader = dataLoader; + this.telemetryClient = telemetryClient; + this.config = config; + this.internalProperties = internalProperties; + this.lockManager = lockManager; + this.system2 = system2; + } + + @Override + public void start() { + boolean isTelemetryActivated = config.getBoolean(SONAR_TELEMETRY_ENABLE.getKey()) + .orElseThrow(() -> new IllegalStateException(String.format("Setting '%s' must be provided.", SONAR_TELEMETRY_URL.getKey()))); + boolean hasOptOut = internalProperties.read(I_PROP_OPT_OUT).isPresent(); + if (!isTelemetryActivated && !hasOptOut) { + optOut(); + internalProperties.write(I_PROP_OPT_OUT, String.valueOf(system2.now())); + LOG.info("Sharing of SonarQube statistics is disabled."); + } + if (isTelemetryActivated && hasOptOut) { + internalProperties.write(I_PROP_OPT_OUT, null); + } + if (!isTelemetryActivated) { + return; + } + LOG.info("Sharing of SonarQube statistics is enabled."); + executorService = Executors.newSingleThreadScheduledExecutor(newThreadFactory()); + int frequencyInSeconds = frequency(); + executorService.scheduleWithFixedDelay(telemetryCommand(), frequencyInSeconds, frequencyInSeconds, TimeUnit.SECONDS); + } + + @Override + public void stop() { + try { + if (executorService == null) { + return; + } + executorService.shutdown(); + executorService.awaitTermination(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + private static ThreadFactory newThreadFactory() { + return new ThreadFactoryBuilder() + .setNameFormat(THREAD_NAME_PREFIX + "%d") + .setPriority(Thread.MIN_PRIORITY) + .build(); + } + + private Runnable telemetryCommand() { + return () -> { + try { + + if (!lockManager.tryLock(LOCK_NAME, lockDuration())) { + return; + } + + long now = system2.now(); + if (shouldUploadStatistics(now)) { + uploadStatistics(); + internalProperties.write(I_PROP_LAST_PING, String.valueOf(startOfDay(now))); + } + } catch (Exception e) { + LOG.debug("Error while checking SonarQube statistics: {}", e.getMessage()); + } + // do not check at start up to exclude test instance which are not up for a long time + }; + } + + private void optOut() { + StringWriter json = new StringWriter(); + try (JsonWriter writer = JsonWriter.of(json)) { + writer.beginObject(); + writer.prop("id", dataLoader.loadServerId()); + writer.endObject(); + } + telemetryClient.optOut(json.toString()); + } + + private void uploadStatistics() throws IOException { + TelemetryData statistics = dataLoader.load(); + StringWriter jsonString = new StringWriter(); + try (JsonWriter json = JsonWriter.of(jsonString)) { + writeTelemetryData(json, statistics); + } + telemetryClient.upload(jsonString.toString()); + } + + private boolean shouldUploadStatistics(long now) { + Optional<Long> lastPing = internalProperties.read(I_PROP_LAST_PING).map(Long::valueOf); + return !lastPing.isPresent() || now - lastPing.get() >= SEVEN_DAYS; + } + + private static long startOfDay(long now) { + return parseDate(formatDate(new Date(now))).getTime(); + } + + private int frequency() { + return config.getInt(SONAR_TELEMETRY_FREQUENCY_IN_SECONDS.getKey()) + .orElseThrow(() -> new IllegalStateException(String.format("Setting '%s' must be provided.", SONAR_TELEMETRY_FREQUENCY_IN_SECONDS))); + } + + private int lockDuration() { + return config.getInt(LOCK_DELAY_SEC).orElse(60); + } +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/telemetry/TelemetryDataLoaderImpl.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/telemetry/TelemetryDataLoaderImpl.java new file mode 100644 index 00000000000..b31f2b30678 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/telemetry/TelemetryDataLoaderImpl.java @@ -0,0 +1,131 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.telemetry; + +import java.sql.DatabaseMetaData; +import java.sql.SQLException; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; +import org.sonar.api.platform.Server; +import org.sonar.api.server.ServerSide; +import org.sonar.core.platform.PlatformEditionProvider; +import org.sonar.core.platform.PluginInfo; +import org.sonar.core.platform.PluginRepository; +import org.sonar.core.util.stream.MoreCollectors; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.measure.SumNclocDbQuery; +import org.sonar.server.es.SearchOptions; +import org.sonar.server.measure.index.ProjectMeasuresIndex; +import org.sonar.server.measure.index.ProjectMeasuresStatistics; +import org.sonar.server.organization.DefaultOrganizationProvider; +import org.sonar.server.property.InternalProperties; +import org.sonar.server.telemetry.TelemetryData.Database; +import org.sonar.server.user.index.UserIndex; +import org.sonar.server.user.index.UserQuery; + +import static java.util.Optional.ofNullable; + +@ServerSide +public class TelemetryDataLoaderImpl implements TelemetryDataLoader { + private final Server server; + private final DbClient dbClient; + private final PluginRepository pluginRepository; + private final UserIndex userIndex; + private final ProjectMeasuresIndex projectMeasuresIndex; + private final PlatformEditionProvider editionProvider; + private final DefaultOrganizationProvider defaultOrganizationProvider; + private final InternalProperties internalProperties; + @CheckForNull + private final LicenseReader licenseReader; + + public TelemetryDataLoaderImpl(Server server, DbClient dbClient, PluginRepository pluginRepository, UserIndex userIndex, ProjectMeasuresIndex projectMeasuresIndex, + PlatformEditionProvider editionProvider, DefaultOrganizationProvider defaultOrganizationProvider, InternalProperties internalProperties) { + this(server, dbClient, pluginRepository, userIndex, projectMeasuresIndex, editionProvider, defaultOrganizationProvider, internalProperties, null); + } + + public TelemetryDataLoaderImpl(Server server, DbClient dbClient, PluginRepository pluginRepository, UserIndex userIndex, ProjectMeasuresIndex projectMeasuresIndex, + PlatformEditionProvider editionProvider, DefaultOrganizationProvider defaultOrganizationProvider, InternalProperties internalProperties, + @Nullable LicenseReader licenseReader) { + this.server = server; + this.dbClient = dbClient; + this.pluginRepository = pluginRepository; + this.userIndex = userIndex; + this.projectMeasuresIndex = projectMeasuresIndex; + this.editionProvider = editionProvider; + this.defaultOrganizationProvider = defaultOrganizationProvider; + this.licenseReader = licenseReader; + this.internalProperties = internalProperties; + } + + private static Database loadDatabaseMetadata(DbSession dbSession) { + try { + DatabaseMetaData metadata = dbSession.getConnection().getMetaData(); + return new Database(metadata.getDatabaseProductName(), metadata.getDatabaseProductVersion()); + } catch (SQLException e) { + throw new IllegalStateException("Fail to get DB metadata", e); + } + } + + @Override + public TelemetryData load() { + TelemetryData.Builder data = TelemetryData.builder(); + + data.setServerId(server.getId()); + data.setVersion(server.getVersion()); + data.setEdition(editionProvider.get()); + ofNullable(licenseReader) + .flatMap(reader -> licenseReader.read()) + .ifPresent(license -> data.setLicenseType(license.getType())); + Function<PluginInfo, String> getVersion = plugin -> plugin.getVersion() == null ? "undefined" : plugin.getVersion().getName(); + Map<String, String> plugins = pluginRepository.getPluginInfos().stream().collect(MoreCollectors.uniqueIndex(PluginInfo::getKey, getVersion)); + data.setPlugins(plugins); + long userCount = userIndex.search(UserQuery.builder().build(), new SearchOptions().setLimit(1)).getTotal(); + data.setUserCount(userCount); + ProjectMeasuresStatistics projectMeasuresStatistics = projectMeasuresIndex.searchTelemetryStatistics(); + data.setProjectMeasuresStatistics(projectMeasuresStatistics); + try (DbSession dbSession = dbClient.openSession(false)) { + data.setDatabase(loadDatabaseMetadata(dbSession)); + data.setUsingBranches(dbClient.branchDao().hasNonMainBranches(dbSession)); + SumNclocDbQuery query = SumNclocDbQuery.builder() + .setOnlyPrivateProjects(false) + .setOrganizationUuid(defaultOrganizationProvider.get().getUuid()) + .build(); + data.setNcloc(dbClient.liveMeasureDao().sumNclocOfBiggestLongLivingBranch(dbSession, query)); + } + + Optional<String> installationDateProperty = internalProperties.read(InternalProperties.INSTALLATION_DATE); + if (installationDateProperty.isPresent()) { + data.setInstallationDate(Long.valueOf(installationDateProperty.get())); + } + Optional<String> installationVersionProperty = internalProperties.read(InternalProperties.INSTALLATION_VERSION); + data.setInstallationVersion(installationVersionProperty.orElse(null)); + + return data.build(); + } + + @Override + public String loadServerId() { + return server.getId(); + } +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/telemetry/package-info.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/telemetry/package-info.java new file mode 100644 index 00000000000..b85d360841b --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/telemetry/package-info.java @@ -0,0 +1,24 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +@ParametersAreNonnullByDefault +package org.sonar.server.telemetry; + +import javax.annotation.ParametersAreNonnullByDefault; + diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/updatecenter/UpdateCenterModule.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/updatecenter/UpdateCenterModule.java new file mode 100644 index 00000000000..d3f07169f61 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/updatecenter/UpdateCenterModule.java @@ -0,0 +1,33 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.updatecenter; + +import org.sonar.core.platform.Module; +import org.sonar.server.plugins.UpdateCenterClient; +import org.sonar.server.plugins.UpdateCenterMatrixFactory; + +public class UpdateCenterModule extends Module { + @Override + protected void configureModule() { + add( + UpdateCenterClient.class, + UpdateCenterMatrixFactory.class); + } +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/updatecenter/package-info.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/updatecenter/package-info.java new file mode 100644 index 00000000000..b89cb43f66a --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/updatecenter/package-info.java @@ -0,0 +1,23 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +@ParametersAreNonnullByDefault +package org.sonar.server.updatecenter; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/util/ClassLoaderUtils.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/util/ClassLoaderUtils.java new file mode 100644 index 00000000000..9e273d66307 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/util/ClassLoaderUtils.java @@ -0,0 +1,114 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.util; + +import com.google.common.base.Throwables; +import java.net.URL; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Enumeration; +import java.util.function.Predicate; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import javax.annotation.Nullable; +import org.apache.commons.lang.StringUtils; +import org.sonar.api.utils.log.Loggers; + +import static java.nio.charset.StandardCharsets.UTF_8; + +public class ClassLoaderUtils { + + private ClassLoaderUtils() { + // only static methods + } + + /** + * Finds files within a given directory and its subdirectories + * + * @param classLoader + * @param rootPath the root directory, for example org/sonar/sqale + * @return a list of relative paths, for example {"org/sonar/sqale/foo/bar.txt}. Never null. + */ + public static Collection<String> listFiles(ClassLoader classLoader, String rootPath) { + return listResources(classLoader, rootPath, path -> !StringUtils.endsWith(path, "/")); + } + + /** + * Finds directories and files within a given directory and its subdirectories. + * + * @param classLoader + * @param rootPath the root directory, for example org/sonar/sqale, or a file in this root directory, for example org/sonar/sqale/index.txt + * @param predicate + * @return a list of relative paths, for example {"org/sonar/sqale", "org/sonar/sqale/foo", "org/sonar/sqale/foo/bar.txt}. Never null. + */ + public static Collection<String> listResources(ClassLoader classLoader, String rootPath, Predicate<String> predicate) { + String jarPath = null; + JarFile jar = null; + try { + Collection<String> paths = new ArrayList<>(); + URL root = classLoader.getResource(rootPath); + if (root != null) { + checkJarFile(root); + + // Path of the root directory + // Examples : + // org/sonar/sqale/index.txt -> rootDirectory is org/sonar/sqale + // org/sonar/sqale/ -> rootDirectory is org/sonar/sqale + // org/sonar/sqale -> rootDirectory is org/sonar/sqale + String rootDirectory = rootPath; + if (StringUtils.substringAfterLast(rootPath, "/").indexOf('.') >= 0) { + rootDirectory = StringUtils.substringBeforeLast(rootPath, "/"); + } + // strip out only the JAR file + jarPath = root.getPath().substring(5, root.getPath().indexOf('!')); + jar = new JarFile(URLDecoder.decode(jarPath, UTF_8.name())); + Enumeration<JarEntry> entries = jar.entries(); + while (entries.hasMoreElements()) { + String name = entries.nextElement().getName(); + if (name.startsWith(rootDirectory) && predicate.test(name)) { + paths.add(name); + } + } + } + return paths; + } catch (Exception e) { + throw Throwables.propagate(e); + } finally { + closeJar(jar, jarPath); + } + } + + private static void closeJar(@Nullable JarFile jar, String jarPath) { + if (jar != null) { + try { + jar.close(); + } catch (Exception e) { + Loggers.get(ClassLoaderUtils.class).error("Fail to close JAR file: " + jarPath, e); + } + } + } + + private static void checkJarFile(URL root) { + if (!"jar".equals(root.getProtocol())) { + throw new IllegalStateException("Unsupported protocol: " + root.getProtocol()); + } + } +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/util/DateCollector.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/util/DateCollector.java new file mode 100644 index 00000000000..446f1a499ab --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/util/DateCollector.java @@ -0,0 +1,46 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.util; + +import javax.annotation.Nullable; + +import java.util.Date; + +public class DateCollector { + + private long maxDate = 0L; + + public void add(@Nullable Date d) { + if (d != null) { + add(d.getTime()); + } + } + + public void add(long date) { + maxDate = Math.max(maxDate, date); + } + + /** + * The most recent collected date. Value is zero if no dates were collected. + */ + public long getMax() { + return maxDate; + } +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/util/TempFolderCleaner.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/util/TempFolderCleaner.java new file mode 100644 index 00000000000..812af0edb85 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/util/TempFolderCleaner.java @@ -0,0 +1,53 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.util; + +import org.sonar.api.Startable; +import org.sonar.api.impl.utils.DefaultTempFolder; +import org.sonar.api.server.ServerSide; +import org.sonar.api.utils.TempFolder; + +@ServerSide +public class TempFolderCleaner implements Startable { + + private TempFolder defaultTempFolder; + + public TempFolderCleaner(TempFolder defaultTempFolder) { + this.defaultTempFolder = defaultTempFolder; + } + + /** + * This method should not be renamed. It follows the naming convention + * defined by IoC container. + */ + @Override + public void start() { + // Nothing to do + } + + /** + * This method should not be renamed. It follows the naming convention + * defined by IoC container. + */ + @Override + public void stop() { + ((DefaultTempFolder) defaultTempFolder).clean(); + } +} diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/util/package-info.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/util/package-info.java new file mode 100644 index 00000000000..d02f184b834 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/util/package-info.java @@ -0,0 +1,23 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +@ParametersAreNonnullByDefault +package org.sonar.server.util; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/sonar-webserver-core/src/main/resources/build.properties b/server/sonar-webserver-core/src/main/resources/build.properties new file mode 100644 index 00000000000..c73bb2bc3ae --- /dev/null +++ b/server/sonar-webserver-core/src/main/resources/build.properties @@ -0,0 +1 @@ +Implementation-Build=@buildNumber@
\ No newline at end of file diff --git a/server/sonar-webserver-core/src/main/resources/com/sonar/sqale/technical-debt-model.xml b/server/sonar-webserver-core/src/main/resources/com/sonar/sqale/technical-debt-model.xml new file mode 100644 index 00000000000..4e3277a44b6 --- /dev/null +++ b/server/sonar-webserver-core/src/main/resources/com/sonar/sqale/technical-debt-model.xml @@ -0,0 +1,206 @@ +<sqale> + <chc> + <key>REUSABILITY</key> + <name>Reusability</name> + <chc> + <key>MODULARITY</key> + <name>Modularity</name> + </chc> + <chc> + <key>REUSABILITY_COMPLIANCE</key> + <name>Reusability Compliance</name> + </chc> + <chc> + <key>TRANSPORTABILITY</key> + <name>Transportability</name> + </chc> + </chc> + <chc> + <key>PORTABILITY</key> + <name>Portability</name> + <chc> + <key>COMPILER_RELATED_PORTABILITY</key> + <name>Compiler</name> + </chc> + <chc> + <key>HARDWARE_RELATED_PORTABILITY</key> + <name>Hardware</name> + </chc> + <chc> + <key>LANGUAGE_RELATED_PORTABILITY</key> + <name>Language</name> + </chc> + <chc> + <key>OS_RELATED_PORTABILITY</key> + <name>OS</name> + </chc> + <chc> + <key>PORTABILITY_COMPLIANCE</key> + <name>Portability Compliance</name> + </chc> + <chc> + <key>SOFTWARE_RELATED_PORTABILITY</key> + <name>Software</name> + </chc> + <chc> + <key>TIME_ZONE_RELATED_PORTABILITY</key> + <name>Time zone</name> + </chc> + </chc> + <chc> + <key>MAINTAINABILITY</key> + <name>Maintainability</name> + <chc> + <key>MAINTAINABILITY_COMPLIANCE</key> + <name>Maintainability Compliance</name> + </chc> + <chc> + <key>READABILITY</key> + <name>Readability</name> + </chc> + <chc> + <key>UNDERSTANDABILITY</key> + <name>Understandability</name> + </chc> + </chc> + <chc> + <key>SECURITY</key> + <name>Security</name> + <chc> + <key>API_ABUSE</key> + <name>API abuse</name> + </chc> + <chc> + <key>ERRORS</key> + <name>Errors</name> + </chc> + <chc> + <key>INPUT_VALIDATION_AND_REPRESENTATION</key> + <name>Input validation and representation</name> + </chc> + <chc> + <key>SECURITY_COMPLIANCE</key> + <name>Security Compliance</name> + </chc> + <chc> + <key>SECURITY_FEATURES</key> + <name>Security features</name> + </chc> + </chc> + <chc> + <key>USABILITY</key> + <name>Usability</name> + <chc> + <key>USABILITY_ACCESSIBILITY</key> + <name>Accessibility</name> + </chc> + <chc> + <key>USABILITY_EASE_OF_USE</key> + <name>Ease of Use</name> + </chc> + <chc> + <key>USABILITY_COMPLIANCE</key> + <name>Usability Compliance</name> + </chc> + </chc> + <chc> + <key>EFFICIENCY</key> + <name>Efficiency</name> + <chc> + <key>CPU_EFFICIENCY</key> + <name>Processor use</name> + </chc> + <chc> + <key>EFFICIENCY_COMPLIANCE</key> + <name>Efficiency Compliance</name> + </chc> + <chc> + <key>MEMORY_EFFICIENCY</key> + <name>Memory use</name> + </chc> + <chc> + <key>NETWORK_USE</key> + <name>Network use</name> + </chc> + </chc> + <chc> + <key>CHANGEABILITY</key> + <name>Changeability</name> + <chc> + <key>ARCHITECTURE_CHANGEABILITY</key> + <name>Architecture</name> + </chc> + <chc> + <key>CHANGEABILITY_COMPLIANCE</key> + <name>Changeability Compliance</name> + </chc> + <chc> + <key>DATA_CHANGEABILITY</key> + <name>Data</name> + </chc> + <chc> + <key>LOGIC_CHANGEABILITY</key> + <name>Logic</name> + </chc> + </chc> + <chc> + <key>RELIABILITY</key> + <name>Reliability</name> + <chc> + <key>ARCHITECTURE_RELIABILITY</key> + <name>Architecture</name> + </chc> + <chc> + <key>DATA_RELIABILITY</key> + <name>Data</name> + </chc> + <chc> + <key>EXCEPTION_HANDLING</key> + <name>Exception handling</name> + </chc> + <chc> + <key>FAULT_TOLERANCE</key> + <name>Fault tolerance</name> + </chc> + <chc> + <key>INSTRUCTION_RELIABILITY</key> + <name>Instruction</name> + </chc> + <chc> + <key>LOGIC_RELIABILITY</key> + <name>Logic</name> + </chc> + <chc> + <key>RELIABILITY_COMPLIANCE</key> + <name>Reliability Compliance</name> + </chc> + <chc> + <key>RESOURCE_RELIABILITY</key> + <name>Resource</name> + </chc> + <chc> + <key>SYNCHRONIZATION_RELIABILITY</key> + <name>Synchronization</name> + </chc> + <chc> + <key>UNIT_TESTS</key> + <name>Unit tests coverage</name> + </chc> + </chc> + <chc> + <key>TESTABILITY</key> + <name>Testability</name> + <chc> + <key>INTEGRATION_TESTABILITY</key> + <name>Integration level</name> + </chc> + <chc> + <key>TESTABILITY_COMPLIANCE</key> + <name>Testability Compliance</name> + </chc> + <chc> + <key>UNIT_TESTABILITY</key> + <name>Unit level</name> + </chc> + </chc> +</sqale> diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/app/WebServerProcessLoggingTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/app/WebServerProcessLoggingTest.java new file mode 100644 index 00000000000..b5612766341 --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/app/WebServerProcessLoggingTest.java @@ -0,0 +1,578 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.app; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.encoder.PatternLayoutEncoder; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.Appender; +import ch.qos.logback.core.AppenderBase; +import ch.qos.logback.core.ConsoleAppender; +import ch.qos.logback.core.FileAppender; +import ch.qos.logback.core.OutputStreamAppender; +import ch.qos.logback.core.encoder.Encoder; +import ch.qos.logback.core.encoder.LayoutWrappingEncoder; +import ch.qos.logback.core.joran.spi.JoranException; +import com.google.common.collect.ImmutableList; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; +import java.util.stream.Stream; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.rules.TemporaryFolder; +import org.sonar.process.Props; +import org.sonar.process.logging.LogbackHelper; +import org.sonar.process.logging.LogbackJsonLayout; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.slf4j.Logger.ROOT_LOGGER_NAME; +import static org.sonar.process.ProcessProperties.Property.PATH_LOGS; + +public class WebServerProcessLoggingTest { + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + private File logDir; + private Props props = new Props(new Properties()); + private WebServerProcessLogging underTest = new WebServerProcessLogging(); + + @Before + public void setUp() throws IOException { + logDir = temp.newFolder(); + props.set(PATH_LOGS.getKey(), logDir.getAbsolutePath()); + } + + @AfterClass + public static void resetLogback() throws JoranException { + new LogbackHelper().resetFromXml("/logback-test.xml"); + } + + @Test + public void do_not_log_to_console() { + LoggerContext ctx = underTest.configure(props); + + Logger root = ctx.getLogger(Logger.ROOT_LOGGER_NAME); + Appender appender = root.getAppender("CONSOLE"); + assertThat(appender).isNull(); + } + + @Test + public void check_level_of_jul() throws IOException { + Props props = new Props(new Properties()); + File dir = temp.newFolder(); + props.set(PATH_LOGS.getKey(), dir.getAbsolutePath()); + props.set("sonar.log.level.web", "TRACE"); + + LoggerContext ctx = underTest.configure(props); + + MemoryAppender memoryAppender = new MemoryAppender(); + memoryAppender.start(); + ctx.getLogger(ROOT_LOGGER_NAME).addAppender(memoryAppender); + + java.util.logging.Logger logger = java.util.logging.Logger.getLogger("com.ms.sqlserver.jdbc.DTV"); + logger.finest("Test"); + memoryAppender.stop(); + assertThat(memoryAppender.getLogs()).hasSize(1); + } + + @Test + public void startup_logger_prints_to_only_to_system_out() { + LoggerContext ctx = underTest.configure(props); + + Logger startup = ctx.getLogger("startup"); + assertThat(startup.isAdditive()).isFalse(); + Appender appender = startup.getAppender("CONSOLE"); + assertThat(appender).isInstanceOf(ConsoleAppender.class); + ConsoleAppender<ILoggingEvent> consoleAppender = (ConsoleAppender<ILoggingEvent>) appender; + assertThat(consoleAppender.getTarget()).isEqualTo("System.out"); + assertThat(consoleAppender.getEncoder()).isInstanceOf(PatternLayoutEncoder.class); + PatternLayoutEncoder patternEncoder = (PatternLayoutEncoder) consoleAppender.getEncoder(); + assertThat(patternEncoder.getPattern()).isEqualTo("%d{yyyy.MM.dd HH:mm:ss} %-5level app[][%logger{20}] %msg%n"); + } + + @Test + public void log_to_web_file() { + LoggerContext ctx = underTest.configure(props); + + Logger root = ctx.getLogger(Logger.ROOT_LOGGER_NAME); + Appender<ILoggingEvent> appender = root.getAppender("file_web"); + assertThat(appender).isInstanceOf(FileAppender.class); + FileAppender fileAppender = (FileAppender) appender; + assertThat(fileAppender.getFile()).isEqualTo(new File(logDir, "web.log").getAbsolutePath()); + assertThat(fileAppender.getEncoder()).isInstanceOf(PatternLayoutEncoder.class); + PatternLayoutEncoder encoder = (PatternLayoutEncoder) fileAppender.getEncoder(); + assertThat(encoder.getPattern()).isEqualTo("%d{yyyy.MM.dd HH:mm:ss} %-5level web[%X{HTTP_REQUEST_ID}][%logger{20}] %msg%n"); + } + + @Test + public void default_level_for_root_logger_is_INFO() { + LoggerContext ctx = underTest.configure(props); + + verifyRootLogLevel(ctx, Level.INFO); + } + + @Test + public void root_logger_level_changes_with_global_property() { + props.set("sonar.log.level", "TRACE"); + + LoggerContext ctx = underTest.configure(props); + + verifyRootLogLevel(ctx, Level.TRACE); + } + + @Test + public void root_logger_level_changes_with_web_property() { + props.set("sonar.log.level.web", "TRACE"); + + LoggerContext ctx = underTest.configure(props); + + verifyRootLogLevel(ctx, Level.TRACE); + } + + @Test + public void root_logger_level_is_configured_from_web_property_over_global_property() { + props.set("sonar.log.level", "TRACE"); + props.set("sonar.log.level.web", "DEBUG"); + + LoggerContext ctx = underTest.configure(props); + + verifyRootLogLevel(ctx, Level.DEBUG); + } + + @Test + public void root_logger_level_changes_with_web_property_and_is_case_insensitive() { + props.set("sonar.log.level.web", "debug"); + + LoggerContext ctx = underTest.configure(props); + + verifyRootLogLevel(ctx, Level.DEBUG); + } + + @Test + public void sql_logger_level_changes_with_global_property_and_is_case_insensitive() { + props.set("sonar.log.level", "InFO"); + + LoggerContext ctx = underTest.configure(props); + + verifySqlLogLevel(ctx, Level.INFO); + } + + @Test + public void sql_logger_level_changes_with_web_property_and_is_case_insensitive() { + props.set("sonar.log.level.web", "TrACe"); + + LoggerContext ctx = underTest.configure(props); + + verifySqlLogLevel(ctx, Level.TRACE); + } + + @Test + public void sql_logger_level_changes_with_web_sql_property_and_is_case_insensitive() { + props.set("sonar.log.level.web.sql", "debug"); + + LoggerContext ctx = underTest.configure(props); + + verifySqlLogLevel(ctx, Level.DEBUG); + } + + @Test + public void sql_logger_level_is_configured_from_web_sql_property_over_web_property() { + props.set("sonar.log.level.web.sql", "debug"); + props.set("sonar.log.level.web", "TRACE"); + + LoggerContext ctx = underTest.configure(props); + + verifySqlLogLevel(ctx, Level.DEBUG); + } + + @Test + public void sql_logger_level_is_configured_from_web_sql_property_over_global_property() { + props.set("sonar.log.level.web.sql", "debug"); + props.set("sonar.log.level", "TRACE"); + + LoggerContext ctx = underTest.configure(props); + + verifySqlLogLevel(ctx, Level.DEBUG); + } + + @Test + public void sql_logger_level_is_configured_from_web_property_over_global_property() { + props.set("sonar.log.level.web", "debug"); + props.set("sonar.log.level", "TRACE"); + + LoggerContext ctx = underTest.configure(props); + + verifySqlLogLevel(ctx, Level.DEBUG); + } + + @Test + public void es_logger_level_changes_with_global_property_and_is_case_insensitive() { + props.set("sonar.log.level", "InFO"); + + LoggerContext ctx = underTest.configure(props); + + verifyEsLogLevel(ctx, Level.INFO); + } + + @Test + public void es_logger_level_changes_with_web_property_and_is_case_insensitive() { + props.set("sonar.log.level.web", "TrACe"); + + LoggerContext ctx = underTest.configure(props); + + verifyEsLogLevel(ctx, Level.TRACE); + } + + @Test + public void es_logger_level_changes_with_web_es_property_and_is_case_insensitive() { + props.set("sonar.log.level.web.es", "debug"); + + LoggerContext ctx = underTest.configure(props); + + verifyEsLogLevel(ctx, Level.DEBUG); + } + + @Test + public void es_logger_level_is_configured_from_web_es_property_over_web_property() { + props.set("sonar.log.level.web.es", "debug"); + props.set("sonar.log.level.web", "TRACE"); + + LoggerContext ctx = underTest.configure(props); + + verifyEsLogLevel(ctx, Level.DEBUG); + } + + @Test + public void es_logger_level_is_configured_from_web_es_property_over_global_property() { + props.set("sonar.log.level.web.es", "debug"); + props.set("sonar.log.level", "TRACE"); + + LoggerContext ctx = underTest.configure(props); + + verifyEsLogLevel(ctx, Level.DEBUG); + } + + @Test + public void es_logger_level_is_configured_from_web_property_over_global_property() { + props.set("sonar.log.level.web", "debug"); + props.set("sonar.log.level", "TRACE"); + + LoggerContext ctx = underTest.configure(props); + + verifyEsLogLevel(ctx, Level.DEBUG); + } + + @Test + public void jmx_logger_level_changes_with_global_property_and_is_case_insensitive() { + props.set("sonar.log.level", "InFO"); + + LoggerContext ctx = underTest.configure(props); + + verifyJmxLogLevel(ctx, Level.INFO); + } + + @Test + public void jmx_logger_level_changes_with_jmx_property_and_is_case_insensitive() { + props.set("sonar.log.level.web", "TrACe"); + + LoggerContext ctx = underTest.configure(props); + + verifyJmxLogLevel(ctx, Level.TRACE); + } + + @Test + public void jmx_logger_level_changes_with_web_jmx_property_and_is_case_insensitive() { + props.set("sonar.log.level.web.jmx", "debug"); + + LoggerContext ctx = underTest.configure(props); + + verifyJmxLogLevel(ctx, Level.DEBUG); + } + + @Test + public void jmx_logger_level_is_configured_from_web_jmx_property_over_web_property() { + props.set("sonar.log.level.web.jmx", "debug"); + props.set("sonar.log.level.web", "TRACE"); + + LoggerContext ctx = underTest.configure(props); + + verifyJmxLogLevel(ctx, Level.DEBUG); + } + + @Test + public void jmx_logger_level_is_configured_from_web_jmx_property_over_global_property() { + props.set("sonar.log.level.web.jmx", "debug"); + props.set("sonar.log.level", "TRACE"); + + LoggerContext ctx = underTest.configure(props); + + verifyJmxLogLevel(ctx, Level.DEBUG); + } + + @Test + public void jmx_logger_level_is_configured_from_web_property_over_global_property() { + props.set("sonar.log.level.web", "debug"); + props.set("sonar.log.level", "TRACE"); + + LoggerContext ctx = underTest.configure(props); + + verifyJmxLogLevel(ctx, Level.DEBUG); + } + + @Test + public void root_logger_level_defaults_to_INFO_if_web_property_has_invalid_value() { + props.set("sonar.log.level.web", "DodoDouh!"); + + LoggerContext ctx = underTest.configure(props); + verifyRootLogLevel(ctx, Level.INFO); + } + + @Test + public void sql_logger_level_defaults_to_INFO_if_web_sql_property_has_invalid_value() { + props.set("sonar.log.level.web.sql", "DodoDouh!"); + + LoggerContext ctx = underTest.configure(props); + verifySqlLogLevel(ctx, Level.INFO); + } + + @Test + public void es_logger_level_defaults_to_INFO_if_web_es_property_has_invalid_value() { + props.set("sonar.log.level.web.es", "DodoDouh!"); + + LoggerContext ctx = underTest.configure(props); + verifyEsLogLevel(ctx, Level.INFO); + } + + @Test + public void jmx_loggers_level_defaults_to_INFO_if_wedb_jmx_property_has_invalid_value() { + props.set("sonar.log.level.web.jmx", "DodoDouh!"); + + LoggerContext ctx = underTest.configure(props); + verifyJmxLogLevel(ctx, Level.INFO); + } + + @Test + public void fail_with_IAE_if_global_property_unsupported_level() { + props.set("sonar.log.level", "ERROR"); + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("log level ERROR in property sonar.log.level is not a supported value (allowed levels are [TRACE, DEBUG, INFO])"); + + underTest.configure(props); + } + + @Test + public void fail_with_IAE_if_web_property_unsupported_level() { + props.set("sonar.log.level.web", "ERROR"); + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("log level ERROR in property sonar.log.level.web is not a supported value (allowed levels are [TRACE, DEBUG, INFO])"); + + underTest.configure(props); + } + + @Test + public void fail_with_IAE_if_web_sql_property_unsupported_level() { + props.set("sonar.log.level.web.sql", "ERROR"); + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("log level ERROR in property sonar.log.level.web.sql is not a supported value (allowed levels are [TRACE, DEBUG, INFO])"); + + underTest.configure(props); + } + + @Test + public void fail_with_IAE_if_web_es_property_unsupported_level() { + props.set("sonar.log.level.web.es", "ERROR"); + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("log level ERROR in property sonar.log.level.web.es is not a supported value (allowed levels are [TRACE, DEBUG, INFO])"); + + underTest.configure(props); + } + + @Test + public void fail_with_IAE_if_web_jmx_property_unsupported_level() { + props.set("sonar.log.level.web.jmx", "ERROR"); + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("log level ERROR in property sonar.log.level.web.jmx is not a supported value (allowed levels are [TRACE, DEBUG, INFO])"); + + underTest.configure(props); + } + + @Test + public void configure_defines_hardcoded_levels() { + LoggerContext context = underTest.configure(props); + + verifyImmutableLogLevels(context); + } + + @Test + public void configure_defines_hardcoded_levels_unchanged_by_global_property() { + props.set("sonar.log.level", "TRACE"); + + LoggerContext context = underTest.configure(props); + + verifyImmutableLogLevels(context); + } + + @Test + public void configure_defines_hardcoded_levels_unchanged_by_ce_property() { + props.set("sonar.log.level.ce", "TRACE"); + + LoggerContext context = underTest.configure(props); + + verifyImmutableLogLevels(context); + } + + @Test + public void configure_turns_off_some_Tomcat_loggers_if_global_log_level_is_not_set() { + LoggerContext context = underTest.configure(props); + + verifyTomcatLoggersLogLevelsOff(context); + } + + @Test + public void configure_turns_off_some_Tomcat_loggers_if_global_log_level_is_INFO() { + props.set("sonar.log.level", "INFO"); + + LoggerContext context = underTest.configure(props); + + verifyTomcatLoggersLogLevelsOff(context); + } + + @Test + public void configure_turns_off_some_Tomcat_loggers_if_global_log_level_is_DEBUG() { + props.set("sonar.log.level", "DEBUG"); + + LoggerContext context = underTest.configure(props); + + verifyTomcatLoggersLogLevelsOff(context); + } + + @Test + public void configure_turns_off_some_Tomcat_loggers_if_global_log_level_is_TRACE() { + props.set("sonar.log.level", "TRACE"); + + LoggerContext context = underTest.configure(props); + + assertThat(context.getLogger("org.apache.catalina.core.ContainerBase").getLevel()).isNull(); + assertThat(context.getLogger("org.apache.catalina.core.StandardContext").getLevel()).isNull(); + assertThat(context.getLogger("org.apache.catalina.core.StandardService").getLevel()).isNull(); + } + + @Test + public void configure_turns_off_some_MsSQL_driver_logger() { + LoggerContext context = underTest.configure(props); + + Stream.of("com.microsoft.sqlserver.jdbc.internals", + "com.microsoft.sqlserver.jdbc.ResultSet", + "com.microsoft.sqlserver.jdbc.Statement", + "com.microsoft.sqlserver.jdbc.Connection") + .forEach(loggerName -> assertThat(context.getLogger(loggerName).getLevel()).isEqualTo(Level.OFF)); + } + + @Test + public void use_json_output() { + props.set("sonar.log.useJsonOutput", "true"); + + LoggerContext context = underTest.configure(props); + + Logger rootLogger = context.getLogger(ROOT_LOGGER_NAME); + OutputStreamAppender appender = (OutputStreamAppender)rootLogger.getAppender("file_web"); + Encoder<ILoggingEvent> encoder = appender.getEncoder(); + assertThat(encoder).isInstanceOf(LayoutWrappingEncoder.class); + assertThat(((LayoutWrappingEncoder)encoder).getLayout()).isInstanceOf(LogbackJsonLayout.class); + } + + private void verifyRootLogLevel(LoggerContext ctx, Level expected) { + Logger rootLogger = ctx.getLogger(ROOT_LOGGER_NAME); + assertThat(rootLogger.getLevel()).isEqualTo(expected); + } + + private void verifySqlLogLevel(LoggerContext ctx, Level expected) { + assertThat(ctx.getLogger("sql").getLevel()).isEqualTo(expected); + } + + private void verifyEsLogLevel(LoggerContext ctx, Level expected) { + assertThat(ctx.getLogger("es").getLevel()).isEqualTo(expected); + } + + private void verifyJmxLogLevel(LoggerContext ctx, Level expected) { + assertThat(ctx.getLogger("javax.management.remote.timeout").getLevel()).isEqualTo(expected); + assertThat(ctx.getLogger("javax.management.remote.misc").getLevel()).isEqualTo(expected); + assertThat(ctx.getLogger("javax.management.remote.rmi").getLevel()).isEqualTo(expected); + assertThat(ctx.getLogger("javax.management.mbeanserver").getLevel()).isEqualTo(expected); + assertThat(ctx.getLogger("sun.rmi.loader").getLevel()).isEqualTo(expected); + assertThat(ctx.getLogger("sun.rmi.transport.tcp").getLevel()).isEqualTo(expected); + assertThat(ctx.getLogger("sun.rmi.transport.misc").getLevel()).isEqualTo(expected); + assertThat(ctx.getLogger("sun.rmi.server.call").getLevel()).isEqualTo(expected); + assertThat(ctx.getLogger("sun.rmi.dgc").getLevel()).isEqualTo(expected); + } + + private void verifyImmutableLogLevels(LoggerContext ctx) { + assertThat(ctx.getLogger("org.apache.ibatis").getLevel()).isEqualTo(Level.WARN); + assertThat(ctx.getLogger("java.sql").getLevel()).isEqualTo(Level.WARN); + assertThat(ctx.getLogger("java.sql.ResultSet").getLevel()).isEqualTo(Level.WARN); + assertThat(ctx.getLogger("org.elasticsearch").getLevel()).isEqualTo(Level.INFO); + assertThat(ctx.getLogger("org.elasticsearch.node").getLevel()).isEqualTo(Level.INFO); + assertThat(ctx.getLogger("org.elasticsearch.http").getLevel()).isEqualTo(Level.INFO); + assertThat(ctx.getLogger("ch.qos.logback").getLevel()).isEqualTo(Level.WARN); + assertThat(ctx.getLogger("org.apache.catalina").getLevel()).isEqualTo(Level.INFO); + assertThat(ctx.getLogger("org.apache.coyote").getLevel()).isEqualTo(Level.INFO); + assertThat(ctx.getLogger("org.apache.jasper").getLevel()).isEqualTo(Level.INFO); + assertThat(ctx.getLogger("org.apache.tomcat").getLevel()).isEqualTo(Level.INFO); + } + + private void verifyTomcatLoggersLogLevelsOff(LoggerContext context) { + assertThat(context.getLogger("org.apache.catalina.core.ContainerBase").getLevel()).isEqualTo(Level.OFF); + assertThat(context.getLogger("org.apache.catalina.core.StandardContext").getLevel()).isEqualTo(Level.OFF); + assertThat(context.getLogger("org.apache.catalina.core.StandardService").getLevel()).isEqualTo(Level.OFF); + } + + public static class MemoryAppender extends AppenderBase<ILoggingEvent> { + private static final List<ILoggingEvent> LOGS = new ArrayList<>(); + + @Override + protected void append(ILoggingEvent eventObject) { + LOGS.add(eventObject); + } + + public List<ILoggingEvent> getLogs() { + return ImmutableList.copyOf(LOGS); + } + + public void clear() { + LOGS.clear(); + } + } +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/debt/DebtModelPluginRepositoryTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/debt/DebtModelPluginRepositoryTest.java new file mode 100644 index 00000000000..5275d006954 --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/debt/DebtModelPluginRepositoryTest.java @@ -0,0 +1,136 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.debt; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.io.Resources; +import java.io.FileInputStream; +import java.io.InputStream; +import java.io.Reader; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import org.apache.commons.io.IOUtils; +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.sonar.api.Plugin; +import org.sonar.core.platform.PluginInfo; +import org.sonar.core.platform.PluginRepository; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class DebtModelPluginRepositoryTest { + + private static final String TEST_XML_PREFIX_PATH = "org/sonar/server/debt/DebtModelPluginRepositoryTest/"; + + DebtModelPluginRepository underTest; + + @Test + public void test_component_initialization() { + // we do have the "csharp-model.xml" file in src/test/resources + PluginInfo csharpPluginMetadata = new PluginInfo("csharp"); + + // but we don' have the "php-model.xml" one + PluginInfo phpPluginMetadata = new PluginInfo("php"); + + PluginRepository repository = mock(PluginRepository.class); + when(repository.getPluginInfos()).thenReturn(Lists.newArrayList(csharpPluginMetadata, phpPluginMetadata)); + FakePlugin fakePlugin = new FakePlugin(); + when(repository.getPluginInstance(anyString())).thenReturn(fakePlugin); + underTest = new DebtModelPluginRepository(repository, TEST_XML_PREFIX_PATH); + + // when + underTest.start(); + + // assert + Collection<String> contributingPluginList = underTest.getContributingPluginList(); + assertThat(contributingPluginList.size()).isEqualTo(2); + assertThat(contributingPluginList).containsOnly("technical-debt", "csharp"); + } + + @Test + public void contributing_plugin_list() { + initModel(); + Collection<String> contributingPluginList = underTest.getContributingPluginList(); + assertThat(contributingPluginList.size()).isEqualTo(2); + assertThat(contributingPluginList).contains("csharp", "java"); + } + + @Test + public void get_content_for_xml_file() { + initModel(); + Reader xmlFileReader = null; + try { + xmlFileReader = underTest.createReaderForXMLFile("csharp"); + assertNotNull(xmlFileReader); + List<String> lines = IOUtils.readLines(xmlFileReader); + assertThat(lines.size()).isEqualTo(25); + assertThat(lines.get(0)).isEqualTo("<sqale>"); + } catch (Exception e) { + fail("Should be able to read the XML file."); + } finally { + IOUtils.closeQuietly(xmlFileReader); + } + } + + @Test + public void return_xml_file_path_for_plugin() { + initModel(); + assertThat(underTest.getXMLFilePath("foo")).isEqualTo(TEST_XML_PREFIX_PATH + "foo-model.xml"); + } + + @Test + public void contain_default_model() { + underTest = new DebtModelPluginRepository(mock(PluginRepository.class)); + underTest.start(); + assertThat(underTest.getContributingPluginKeyToClassLoader().keySet()).containsOnly("technical-debt"); + } + + private void initModel() { + Map<String, ClassLoader> contributingPluginKeyToClassLoader = Maps.newHashMap(); + contributingPluginKeyToClassLoader.put("csharp", newClassLoader()); + contributingPluginKeyToClassLoader.put("java", newClassLoader()); + underTest = new DebtModelPluginRepository(contributingPluginKeyToClassLoader, TEST_XML_PREFIX_PATH); + } + + private ClassLoader newClassLoader() { + ClassLoader loader = mock(ClassLoader.class); + when(loader.getResourceAsStream(anyString())).thenAnswer(new Answer<InputStream>() { + public InputStream answer(InvocationOnMock invocation) throws Throwable { + return new FileInputStream(Resources.getResource((String) invocation.getArguments()[0]).getPath()); + } + }); + return loader; + } + + class FakePlugin implements Plugin { + @Override public void define(Context context) { + + } + } + +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/debt/DebtModelXMLExporterTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/debt/DebtModelXMLExporterTest.java new file mode 100644 index 00000000000..f5e984c9fa3 --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/debt/DebtModelXMLExporterTest.java @@ -0,0 +1,82 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.debt; + +import com.google.common.io.Resources; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.List; +import org.apache.commons.lang.SystemUtils; +import org.junit.Test; +import org.sonar.api.rule.RuleKey; +import org.sonar.api.server.debt.DebtRemediationFunction; + +import static com.google.common.collect.Lists.newArrayList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.server.debt.DebtModelXMLExporter.RuleDebt; + +public class DebtModelXMLExporterTest { + + DebtModelXMLExporter underTest = new DebtModelXMLExporter(); + + @Test + public void export_empty() { + assertThat(underTest.export(Collections.emptyList())).isEqualTo("<sqale/>" + SystemUtils.LINE_SEPARATOR); + } + + @Test + public void export_xml() throws Exception { + List<RuleDebt> rules = newArrayList( + new RuleDebt().setRuleKey(RuleKey.of("checkstyle", "Regexp")) + .setFunction(DebtRemediationFunction.Type.LINEAR_OFFSET.name()).setCoefficient("3d").setOffset("15min") + ); + + assertThat(underTest.export(rules)).isXmlEqualTo(getFileContent("export_xml.xml")); + } + + @Test + public void pretty_print_exported_xml() { + List<RuleDebt> rules = newArrayList( + new RuleDebt().setRuleKey(RuleKey.of("checkstyle", "Regexp")) + .setFunction(DebtRemediationFunction.Type.LINEAR.name()).setCoefficient("3d") + ); + assertThat(underTest.export(rules)).isEqualTo( + "<sqale>" + SystemUtils.LINE_SEPARATOR + + " <chc>" + SystemUtils.LINE_SEPARATOR + + " <rule-repo>checkstyle</rule-repo>" + SystemUtils.LINE_SEPARATOR + + " <rule-key>Regexp</rule-key>" + SystemUtils.LINE_SEPARATOR + + " <prop>" + SystemUtils.LINE_SEPARATOR + + " <key>remediationFunction</key>" + SystemUtils.LINE_SEPARATOR + + " <txt>LINEAR</txt>" + SystemUtils.LINE_SEPARATOR + + " </prop>" + SystemUtils.LINE_SEPARATOR + + " <prop>" + SystemUtils.LINE_SEPARATOR + + " <key>remediationFactor</key>" + SystemUtils.LINE_SEPARATOR + + " <val>3</val>" + SystemUtils.LINE_SEPARATOR + + " <txt>d</txt>" + SystemUtils.LINE_SEPARATOR + + " </prop>" + SystemUtils.LINE_SEPARATOR + + " </chc>" + SystemUtils.LINE_SEPARATOR + + "</sqale>" + SystemUtils.LINE_SEPARATOR + ); + } + + private static String getFileContent(String file) throws Exception { + return Resources.toString(Resources.getResource(DebtModelXMLExporterTest.class, "DebtModelXMLExporterTest/" + file), StandardCharsets.UTF_8); + } +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/debt/DebtRulesXMLImporterTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/debt/DebtRulesXMLImporterTest.java new file mode 100644 index 00000000000..ea78946bd40 --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/debt/DebtRulesXMLImporterTest.java @@ -0,0 +1,240 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.debt; + +import com.google.common.io.Resources; +import java.nio.charset.StandardCharsets; +import java.util.List; +import org.junit.Test; +import org.sonar.api.rule.RuleKey; +import org.sonar.api.server.debt.DebtRemediationFunction; +import org.sonar.api.utils.ValidationMessages; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.fail; +import static org.sonar.server.debt.DebtModelXMLExporter.RuleDebt; + +public class DebtRulesXMLImporterTest { + + ValidationMessages validationMessages = ValidationMessages.create(); + DebtRulesXMLImporter underTest = new DebtRulesXMLImporter(); + + @Test + public void import_rules() throws Exception { + String xml = getFileContent("import_rules.xml"); + + List<RuleDebt> results = underTest.importXML(xml, validationMessages); + + assertThat(results).extracting("ruleKey").containsOnly(RuleKey.of("javasquid", "rule1"), RuleKey.of("javasquid", "rule2")); + assertThat(validationMessages.getErrors()).isEmpty(); + assertThat(validationMessages.getWarnings()).isEmpty(); + } + + @Test + public void import_rules_with_deprecated_quality_model_format() throws Exception { + String xml = getFileContent("import_rules_with_deprecated_quality_model_format.xml"); + + List<RuleDebt> results = underTest.importXML(xml, validationMessages); + + assertThat(results).extracting("ruleKey").containsOnly(RuleKey.of("javasquid", "rule1"), RuleKey.of("javasquid", "rule2")); + assertThat(validationMessages.getErrors()).isEmpty(); + assertThat(validationMessages.getWarnings()).isEmpty(); + } + + @Test + public void import_linear() throws Exception { + String xml = getFileContent("import_linear.xml"); + + List<RuleDebt> results = underTest.importXML(xml, validationMessages); + assertThat(results).hasSize(1); + + RuleDebt ruleDebt = results.get(0); + assertThat(ruleDebt.ruleKey()).isEqualTo(RuleKey.of("checkstyle", "Regexp")); + assertThat(ruleDebt.function()).isEqualTo(DebtRemediationFunction.Type.LINEAR.name()); + assertThat(ruleDebt.coefficient()).isEqualTo("3h"); + assertThat(ruleDebt.offset()).isNull(); + } + + @Test + public void import_linear_having_offset_to_zero() throws Exception { + String xml = getFileContent("import_linear_having_offset_to_zero.xml"); + + List<RuleDebt> results = underTest.importXML(xml, validationMessages); + assertThat(results).hasSize(1); + + RuleDebt ruleDebt = results.get(0); + assertThat(ruleDebt.ruleKey()).isEqualTo(RuleKey.of("checkstyle", "Regexp")); + assertThat(ruleDebt.function()).isEqualTo(DebtRemediationFunction.Type.LINEAR.name()); + assertThat(ruleDebt.coefficient()).isEqualTo("3h"); + assertThat(ruleDebt.offset()).isNull(); + } + + @Test + public void import_linear_with_offset() throws Exception { + String xml = getFileContent("import_linear_with_offset.xml"); + + List<RuleDebt> results = underTest.importXML(xml, validationMessages); + assertThat(results).hasSize(1); + + RuleDebt ruleDebt = results.get(0); + assertThat(ruleDebt.function()).isEqualTo(DebtRemediationFunction.Type.LINEAR_OFFSET.name()); + assertThat(ruleDebt.coefficient()).isEqualTo("3h"); + assertThat(ruleDebt.offset()).isEqualTo("1min"); + } + + @Test + public void import_constant_issue() throws Exception { + String xml = getFileContent("import_constant_issue.xml"); + + List<RuleDebt> results = underTest.importXML(xml, validationMessages); + assertThat(results).hasSize(1); + + RuleDebt ruleDebt = results.get(0); + assertThat(ruleDebt.function()).isEqualTo(DebtRemediationFunction.Type.CONSTANT_ISSUE.name()); + assertThat(ruleDebt.coefficient()).isNull(); + assertThat(ruleDebt.offset()).isEqualTo("3d"); + } + + @Test + public void use_default_unit_when_no_unit() throws Exception { + String xml = getFileContent("use_default_unit_when_no_unit.xml"); + + List<RuleDebt> results = underTest.importXML(xml, validationMessages); + assertThat(results).hasSize(1); + + RuleDebt ruleDebt = results.get(0); + assertThat(ruleDebt.function()).isEqualTo(DebtRemediationFunction.Type.LINEAR_OFFSET.name()); + assertThat(ruleDebt.coefficient()).isEqualTo("3d"); + assertThat(ruleDebt.offset()).isEqualTo("1d"); + } + + @Test + public void replace_mn_by_min() throws Exception { + String xml = getFileContent("replace_mn_by_min.xml"); + + List<RuleDebt> results = underTest.importXML(xml, validationMessages); + assertThat(results).hasSize(1); + + RuleDebt ruleDebt = results.get(0); + assertThat(ruleDebt.function()).isEqualTo(DebtRemediationFunction.Type.LINEAR.name()); + assertThat(ruleDebt.coefficient()).isEqualTo("3min"); + assertThat(ruleDebt.offset()).isNull(); + } + + @Test + public void read_integer() throws Exception { + String xml = getFileContent("read_integer.xml"); + + List<RuleDebt> results = underTest.importXML(xml, validationMessages); + assertThat(results).hasSize(1); + + RuleDebt ruleDebt = results.get(0); + assertThat(ruleDebt.ruleKey()).isEqualTo(RuleKey.of("checkstyle", "Regexp")); + assertThat(ruleDebt.function()).isEqualTo(DebtRemediationFunction.Type.LINEAR.name()); + assertThat(ruleDebt.coefficient()).isEqualTo("3h"); + assertThat(ruleDebt.offset()).isNull(); + } + + @Test + public void convert_deprecated_linear_with_threshold_function_by_linear_function() throws Exception { + String xml = getFileContent("convert_deprecated_linear_with_threshold_function_by_linear_function.xml"); + + List<RuleDebt> results = underTest.importXML(xml, validationMessages); + assertThat(results).hasSize(1); + + RuleDebt ruleDebt = results.get(0); + assertThat(ruleDebt.function()).isEqualTo(DebtRemediationFunction.Type.LINEAR.name()); + assertThat(ruleDebt.coefficient()).isEqualTo("3h"); + assertThat(ruleDebt.offset()).isNull(); + + assertThat(validationMessages.getWarnings()).isNotEmpty(); + } + + @Test + public void convert_constant_per_issue_with_coefficient_by_constant_per_issue_with_offset() throws Exception { + String xml = getFileContent("convert_constant_per_issue_with_coefficient_by_constant_per_issue_with_offset.xml"); + + List<RuleDebt> results = underTest.importXML(xml, validationMessages); + assertThat(results).hasSize(1); + + RuleDebt ruleDebt = results.get(0); + assertThat(ruleDebt.function()).isEqualTo(DebtRemediationFunction.Type.CONSTANT_ISSUE.name()); + assertThat(ruleDebt.coefficient()).isNull(); + assertThat(ruleDebt.offset()).isEqualTo("3h"); + } + + @Test + public void ignore_remediation_cost_having_zero_value() throws Exception { + String xml = getFileContent("ignore_remediation_cost_having_zero_value.xml"); + + List<RuleDebt> results = underTest.importXML(xml, validationMessages); + assertThat(results).isEmpty(); + } + + @Test + public void ignore_deprecated_constant_per_file_function() throws Exception { + String xml = getFileContent("ignore_deprecated_constant_per_file_function.xml"); + + List<RuleDebt> results = underTest.importXML(xml, validationMessages); + assertThat(results).isEmpty(); + + assertThat(validationMessages.getWarnings()).isNotEmpty(); + } + + @Test + public void import_badly_formatted_xml() throws Exception { + String xml = getFileContent("import_badly_formatted_xml.xml"); + + List<RuleDebt> results = underTest.importXML(xml, validationMessages); + assertThat(results).hasSize(1); + + RuleDebt ruleDebt = results.get(0); + assertThat(ruleDebt.ruleKey()).isEqualTo(RuleKey.of("checkstyle", "Regexp")); + assertThat(ruleDebt.function()).isEqualTo(DebtRemediationFunction.Type.LINEAR.name()); + assertThat(ruleDebt.coefficient()).isEqualTo("3h"); + assertThat(ruleDebt.offset()).isNull(); + } + + @Test + public void ignore_invalid_value() throws Exception { + String xml = getFileContent("ignore_invalid_value.xml"); + List<RuleDebt> results = underTest.importXML(xml, validationMessages); + assertThat(results).isEmpty(); + + assertThat(validationMessages.getErrors()).isNotEmpty(); + } + + @Test + public void fail_on_bad_xml() throws Exception { + String xml = getFileContent("fail_on_bad_xml.xml"); + + try { + underTest.importXML(xml, validationMessages); + fail(); + } catch (Exception e) { + assertThat(e).isInstanceOf(IllegalStateException.class); + } + } + + private String getFileContent(String file) throws Exception { + return Resources.toString(Resources.getResource(getClass(), "DebtRulesXMLImporterTest/" + file), + StandardCharsets.UTF_8); + } +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/es/IndexerStartupTaskTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/es/IndexerStartupTaskTest.java new file mode 100644 index 00000000000..de10b8e1905 --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/es/IndexerStartupTaskTest.java @@ -0,0 +1,93 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.es; + +import com.google.common.collect.ImmutableSet; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mockito; +import org.sonar.api.config.internal.MapSettings; +import org.sonar.server.es.metadata.MetadataIndex; +import org.sonar.server.es.metadata.MetadataIndexImpl; +import org.sonar.server.es.newindex.FakeIndexDefinition; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.sonar.server.es.newindex.FakeIndexDefinition.TYPE_FAKE; + +public class IndexerStartupTaskTest { + + @Rule + public EsTester es = EsTester.createCustom(new FakeIndexDefinition()); + + private final MapSettings settings = new MapSettings(); + private final MetadataIndex metadataIndex = mock(MetadataIndexImpl.class); + private final StartupIndexer indexer = mock(StartupIndexer.class); + private final IndexerStartupTask underTest = new IndexerStartupTask(es.client(), settings.asConfig(), metadataIndex, indexer); + + @Before + public void setUp() throws Exception { + doReturn(ImmutableSet.of(TYPE_FAKE)).when(indexer).getIndexTypes(); + } + + @Test + public void index_if_not_initialized() { + doReturn(false).when(metadataIndex).getInitialized(TYPE_FAKE); + + underTest.execute(); + + verify(indexer).getIndexTypes(); + verify(indexer).indexOnStartup(Mockito.eq(ImmutableSet.of(TYPE_FAKE))); + } + + @Test + public void set_initialized_after_indexation() { + doReturn(false).when(metadataIndex).getInitialized(TYPE_FAKE); + + underTest.execute(); + + verify(metadataIndex).setInitialized(eq(TYPE_FAKE), eq(true)); + } + + @Test + public void do_not_index_if_already_initialized() { + doReturn(true).when(metadataIndex).getInitialized(TYPE_FAKE); + + underTest.execute(); + + verify(indexer).getIndexTypes(); + verifyNoMoreInteractions(indexer); + } + + @Test + public void do_not_index_if_indexes_are_disabled() { + settings.setProperty("sonar.internal.es.disableIndexes", "true"); + es.putDocuments(TYPE_FAKE, new FakeDoc()); + + underTest.execute(); + + // do not index + verifyNoMoreInteractions(indexer); + } +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/es/MigrationEsClientImplTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/es/MigrationEsClientImplTest.java new file mode 100644 index 00000000000..4f6633793b6 --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/es/MigrationEsClientImplTest.java @@ -0,0 +1,125 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.es; + +import com.google.common.collect.ImmutableMap; +import java.util.Iterator; +import java.util.Map; +import javax.annotation.CheckForNull; +import org.elasticsearch.cluster.metadata.MappingMetaData; +import org.elasticsearch.common.collect.ImmutableOpenMap; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.api.config.internal.MapSettings; +import org.sonar.api.utils.log.LogTester; +import org.sonar.api.utils.log.LoggerLevel; +import org.sonar.server.platform.db.migration.es.MigrationEsClient; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.server.es.newindex.SettingsConfiguration.newBuilder; + +public class MigrationEsClientImplTest { + @Rule + public LogTester logTester = new LogTester(); + @Rule + public EsTester es = EsTester.createCustom( + new SimpleIndexDefinition("as"), + new SimpleIndexDefinition("bs"), + new SimpleIndexDefinition("cs")); + + private MigrationEsClient underTest = new MigrationEsClientImpl(es.client()); + + @Test + public void delete_existing_index() { + underTest.deleteIndexes("as"); + + assertThat(loadExistingIndices()) + .toIterable() + .doesNotContain("as") + .contains("bs", "cs"); + assertThat(logTester.logs(LoggerLevel.INFO)) + .contains("Drop Elasticsearch index [as]"); + } + + @Test + public void delete_index_that_does_not_exist() { + underTest.deleteIndexes("as", "xxx", "cs"); + + assertThat(loadExistingIndices()) + .toIterable() + .doesNotContain("as", "cs") + .contains("bs"); + assertThat(logTester.logs(LoggerLevel.INFO)) + .contains("Drop Elasticsearch index [as]", "Drop Elasticsearch index [cs]") + .doesNotContain("Drop Elasticsearch index [xxx]"); + } + + @Test + public void addMappingToExistingIndex() { + Map<String, String> mappingOptions = ImmutableMap.of("norms", "false"); + underTest.addMappingToExistingIndex("as", "s", "new_field", "keyword", mappingOptions); + + assertThat(loadExistingIndices()).toIterable().contains("as"); + ImmutableOpenMap<String, ImmutableOpenMap<String, MappingMetaData>> mappings = mappings(); + MappingMetaData mapping = mappings.get("as").get("s"); + assertThat(countMappingFields(mapping)).isEqualTo(1); + assertThat(field(mapping, "new_field")).isNotNull(); + + assertThat(logTester.logs(LoggerLevel.INFO)) + .contains("Add mapping [new_field] to Elasticsearch index [as]"); + assertThat(underTest.getUpdatedIndices()).containsExactly("as"); + } + + private Iterator<String> loadExistingIndices() { + return es.client().nativeClient().admin().indices().prepareGetMappings().get().mappings().keysIt(); + } + + private ImmutableOpenMap<String, ImmutableOpenMap<String, MappingMetaData>> mappings() { + return es.client().nativeClient().admin().indices().prepareGetMappings().get().mappings(); + } + + @CheckForNull + @SuppressWarnings("unchecked") + private Map<String, Object> field(MappingMetaData mapping, String field) { + Map<String, Object> props = (Map<String, Object>) mapping.getSourceAsMap().get("properties"); + return (Map<String, Object>) props.get(field); + } + + private int countMappingFields(MappingMetaData mapping) { + return ((Map) mapping.getSourceAsMap().get("properties")).size(); + } + + private static class SimpleIndexDefinition implements IndexDefinition { + private final String indexName; + + public SimpleIndexDefinition(String indexName) { + this.indexName = indexName; + } + + @Override + public void define(IndexDefinitionContext context) { + IndexType.IndexMainType mainType = IndexType.main(Index.simple(indexName), indexName.substring(1)); + context.create( + mainType.getIndex(), + newBuilder(new MapSettings().asConfig()).build()) + .createTypeMapping(mainType); + } + } +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexTest.java new file mode 100644 index 00000000000..741d823a6e8 --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexTest.java @@ -0,0 +1,1509 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.measure.index; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.tngtech.java.junit.dataprovider.DataProvider; +import com.tngtech.java.junit.dataprovider.DataProviderRunner; +import com.tngtech.java.junit.dataprovider.UseDataProvider; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.IntStream; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.sonar.api.utils.System2; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.component.ComponentTesting; +import org.sonar.db.organization.OrganizationDto; +import org.sonar.db.organization.OrganizationTesting; +import org.sonar.db.user.GroupDto; +import org.sonar.db.user.UserDto; +import org.sonar.server.es.EsTester; +import org.sonar.server.es.Facets; +import org.sonar.server.es.SearchIdResult; +import org.sonar.server.es.SearchOptions; +import org.sonar.server.measure.index.ProjectMeasuresQuery.MetricCriterion; +import org.sonar.server.permission.index.IndexPermissions; +import org.sonar.server.permission.index.PermissionIndexerTester; +import org.sonar.server.permission.index.WebAuthorizationTypeSupport; +import org.sonar.server.tester.UserSessionRule; + +import static com.google.common.collect.Lists.newArrayList; +import static com.google.common.collect.Sets.newHashSet; +import static java.util.Arrays.asList; +import static java.util.Arrays.stream; +import static java.util.Collections.singletonList; +import static java.util.stream.Collectors.toList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; +import static org.sonar.api.measures.CoreMetrics.ALERT_STATUS_KEY; +import static org.sonar.api.measures.CoreMetrics.COVERAGE_KEY; +import static org.sonar.api.measures.Metric.Level.ERROR; +import static org.sonar.api.measures.Metric.Level.OK; +import static org.sonar.api.measures.Metric.Level.WARN; +import static org.sonar.api.resources.Qualifiers.PROJECT; +import static org.sonar.db.component.ComponentTesting.newPrivateProjectDto; +import static org.sonar.db.user.GroupTesting.newGroupDto; +import static org.sonar.db.user.UserTesting.newUserDto; +import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_TAGS; +import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.TYPE_PROJECT_MEASURES; +import static org.sonar.server.measure.index.ProjectMeasuresQuery.Operator; + +@RunWith(DataProviderRunner.class) +public class ProjectMeasuresIndexTest { + + private static final String MAINTAINABILITY_RATING = "sqale_rating"; + private static final String NEW_MAINTAINABILITY_RATING_KEY = "new_maintainability_rating"; + private static final String RELIABILITY_RATING = "reliability_rating"; + private static final String NEW_RELIABILITY_RATING = "new_reliability_rating"; + private static final String SECURITY_RATING = "security_rating"; + private static final String NEW_SECURITY_RATING = "new_security_rating"; + private static final String COVERAGE = "coverage"; + private static final String NEW_COVERAGE = "new_coverage"; + private static final String DUPLICATION = "duplicated_lines_density"; + private static final String NEW_DUPLICATION = "new_duplicated_lines_density"; + private static final String NCLOC = "ncloc"; + private static final String NEW_LINES = "new_lines"; + private static final String LANGUAGES = "languages"; + + private static final OrganizationDto ORG = OrganizationTesting.newOrganizationDto(); + private static final ComponentDto PROJECT1 = ComponentTesting.newPrivateProjectDto(ORG).setUuid("Project-1").setName("Project 1").setDbKey("key-1"); + private static final ComponentDto PROJECT2 = ComponentTesting.newPrivateProjectDto(ORG).setUuid("Project-2").setName("Project 2").setDbKey("key-2"); + private static final ComponentDto PROJECT3 = ComponentTesting.newPrivateProjectDto(ORG).setUuid("Project-3").setName("Project 3").setDbKey("key-3"); + private static final UserDto USER1 = newUserDto(); + private static final UserDto USER2 = newUserDto(); + private static final GroupDto GROUP1 = newGroupDto(); + private static final GroupDto GROUP2 = newGroupDto(); + + @Rule + public EsTester es = EsTester.create(); + @Rule + public ExpectedException expectedException = ExpectedException.none(); + @Rule + public UserSessionRule userSession = UserSessionRule.standalone(); + + @DataProvider + public static Object[][] rating_metric_keys() { + return new Object[][] {{MAINTAINABILITY_RATING}, {NEW_MAINTAINABILITY_RATING_KEY}, {RELIABILITY_RATING}, {NEW_RELIABILITY_RATING}, {SECURITY_RATING}, {NEW_SECURITY_RATING}}; + } + + private ProjectMeasuresIndexer projectMeasureIndexer = new ProjectMeasuresIndexer(null, es.client()); + private PermissionIndexerTester authorizationIndexer = new PermissionIndexerTester(es, projectMeasureIndexer); + private ProjectMeasuresIndex underTest = new ProjectMeasuresIndex(es.client(), new WebAuthorizationTypeSupport(userSession), System2.INSTANCE); + + @Test + public void return_empty_if_no_projects() { + assertNoResults(new ProjectMeasuresQuery()); + } + + @Test + public void default_sort_is_by_ascending_case_insensitive_name_then_by_key() { + ComponentDto windows = ComponentTesting.newPrivateProjectDto(ORG).setUuid("windows").setName("Windows").setDbKey("project1"); + ComponentDto apachee = ComponentTesting.newPrivateProjectDto(ORG).setUuid("apachee").setName("apachee").setDbKey("project2"); + ComponentDto apache1 = ComponentTesting.newPrivateProjectDto(ORG).setUuid("apache-1").setName("Apache").setDbKey("project3"); + ComponentDto apache2 = ComponentTesting.newPrivateProjectDto(ORG).setUuid("apache-2").setName("Apache").setDbKey("project4"); + index(newDoc(windows), newDoc(apachee), newDoc(apache1), newDoc(apache2)); + + assertResults(new ProjectMeasuresQuery(), apache1, apache2, apachee, windows); + } + + @Test + public void sort_by_insensitive_name() { + ComponentDto windows = ComponentTesting.newPrivateProjectDto(ORG).setUuid("windows").setName("Windows"); + ComponentDto apachee = ComponentTesting.newPrivateProjectDto(ORG).setUuid("apachee").setName("apachee"); + ComponentDto apache = ComponentTesting.newPrivateProjectDto(ORG).setUuid("apache").setName("Apache"); + index(newDoc(windows), newDoc(apachee), newDoc(apache)); + + assertResults(new ProjectMeasuresQuery().setSort("name").setAsc(true), apache, apachee, windows); + assertResults(new ProjectMeasuresQuery().setSort("name").setAsc(false), windows, apachee, apache); + } + + @Test + public void sort_by_ncloc() { + index( + newDoc(PROJECT1, NCLOC, 15_000d), + newDoc(PROJECT2, NCLOC, 30_000d), + newDoc(PROJECT3, NCLOC, 1_000d)); + + assertResults(new ProjectMeasuresQuery().setSort("ncloc").setAsc(true), PROJECT3, PROJECT1, PROJECT2); + assertResults(new ProjectMeasuresQuery().setSort("ncloc").setAsc(false), PROJECT2, PROJECT1, PROJECT3); + } + + @Test + public void sort_by_a_metric_then_by_name_then_by_key() { + ComponentDto windows = ComponentTesting.newPrivateProjectDto(ORG).setUuid("windows").setName("Windows").setDbKey("project1"); + ComponentDto apachee = ComponentTesting.newPrivateProjectDto(ORG).setUuid("apachee").setName("apachee").setDbKey("project2"); + ComponentDto apache1 = ComponentTesting.newPrivateProjectDto(ORG).setUuid("apache-1").setName("Apache").setDbKey("project3"); + ComponentDto apache2 = ComponentTesting.newPrivateProjectDto(ORG).setUuid("apache-2").setName("Apache").setDbKey("project4"); + index( + newDoc(windows, NCLOC, 10_000d), + newDoc(apachee, NCLOC, 5_000d), + newDoc(apache1, NCLOC, 5_000d), + newDoc(apache2, NCLOC, 5_000d)); + + assertResults(new ProjectMeasuresQuery().setSort("ncloc").setAsc(true), apache1, apache2, apachee, windows); + assertResults(new ProjectMeasuresQuery().setSort("ncloc").setAsc(false), windows, apache1, apache2, apachee); + } + + @Test + public void sort_by_quality_gate_status() { + ComponentDto project4 = ComponentTesting.newPrivateProjectDto(ORG).setUuid("Project-4").setName("Project 4").setDbKey("key-4"); + index( + newDoc(PROJECT1).setQualityGateStatus(OK.name()), + newDoc(PROJECT2).setQualityGateStatus(ERROR.name()), + newDoc(project4).setQualityGateStatus(OK.name())); + + assertResults(new ProjectMeasuresQuery().setSort("alert_status").setAsc(true), PROJECT1, project4, PROJECT2); + assertResults(new ProjectMeasuresQuery().setSort("alert_status").setAsc(false), PROJECT2, PROJECT1, project4); + } + + @Test + public void sort_by_quality_gate_status_then_by_name_then_by_key() { + ComponentDto windows = ComponentTesting.newPrivateProjectDto(ORG).setUuid("windows").setName("Windows").setDbKey("project1"); + ComponentDto apachee = ComponentTesting.newPrivateProjectDto(ORG).setUuid("apachee").setName("apachee").setDbKey("project2"); + ComponentDto apache1 = ComponentTesting.newPrivateProjectDto(ORG).setUuid("apache-1").setName("Apache").setDbKey("project3"); + ComponentDto apache2 = ComponentTesting.newPrivateProjectDto(ORG).setUuid("apache-2").setName("Apache").setDbKey("project4"); + index( + newDoc(windows).setQualityGateStatus(ERROR.name()), + newDoc(apachee).setQualityGateStatus(OK.name()), + newDoc(apache1).setQualityGateStatus(OK.name()), + newDoc(apache2).setQualityGateStatus(OK.name())); + + assertResults(new ProjectMeasuresQuery().setSort("alert_status").setAsc(true), apache1, apache2, apachee, windows); + assertResults(new ProjectMeasuresQuery().setSort("alert_status").setAsc(false), windows, apache1, apache2, apachee); + } + + @Test + public void paginate_results() { + IntStream.rangeClosed(1, 9) + .forEach(i -> index(newDoc(newPrivateProjectDto(ORG, "P" + i)))); + + SearchIdResult<String> result = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().setPage(2, 3)); + + assertThat(result.getIds()).containsExactly("P4", "P5", "P6"); + assertThat(result.getTotal()).isEqualTo(9); + } + + @Test + public void filter_with_lower_than() { + index( + newDoc(PROJECT1, COVERAGE, 79d, NCLOC, 10_000d), + newDoc(PROJECT2, COVERAGE, 80d, NCLOC, 10_000d), + newDoc(PROJECT3, COVERAGE, 81d, NCLOC, 10_000d)); + + ProjectMeasuresQuery query = new ProjectMeasuresQuery() + .addMetricCriterion(MetricCriterion.create(COVERAGE, Operator.LT, 80d)); + + assertResults(query, PROJECT1); + } + + @Test + public void filter_with_lower_than_or_equals() { + index( + newDoc(PROJECT1, COVERAGE, 79d, NCLOC, 10_000d), + newDoc(PROJECT2, COVERAGE, 80d, NCLOC, 10_000d), + newDoc(PROJECT3, COVERAGE, 81d, NCLOC, 10_000d)); + + ProjectMeasuresQuery query = new ProjectMeasuresQuery() + .addMetricCriterion(MetricCriterion.create(COVERAGE, Operator.LTE, 80d)); + + assertResults(query, PROJECT1, PROJECT2); + } + + @Test + public void filter_with_greater_than() { + index( + newDoc(PROJECT1, COVERAGE, 80d, NCLOC, 30_000d), + newDoc(PROJECT2, COVERAGE, 80d, NCLOC, 30_001d), + newDoc(PROJECT3, COVERAGE, 80d, NCLOC, 30_001d)); + + ProjectMeasuresQuery query = new ProjectMeasuresQuery().addMetricCriterion(MetricCriterion.create(NCLOC, Operator.GT, 30_000d)); + assertResults(query, PROJECT2, PROJECT3); + + query = new ProjectMeasuresQuery().addMetricCriterion(MetricCriterion.create(NCLOC, Operator.GT, 100_000d)); + assertNoResults(query); + } + + @Test + public void filter_with_greater_than_or_equals() { + index( + newDoc(PROJECT1, COVERAGE, 80d, NCLOC, 30_000d), + newDoc(PROJECT2, COVERAGE, 80d, NCLOC, 30_001d), + newDoc(PROJECT3, COVERAGE, 80d, NCLOC, 30_001d)); + + ProjectMeasuresQuery query = new ProjectMeasuresQuery().addMetricCriterion(MetricCriterion.create(NCLOC, Operator.GTE, 30_001d)); + assertResults(query, PROJECT2, PROJECT3); + + query = new ProjectMeasuresQuery().addMetricCriterion(MetricCriterion.create(NCLOC, Operator.GTE, 100_000d)); + assertNoResults(query); + } + + @Test + public void filter_with_equals() { + index( + newDoc(PROJECT1, COVERAGE, 79d, NCLOC, 10_000d), + newDoc(PROJECT2, COVERAGE, 80d, NCLOC, 10_000d), + newDoc(PROJECT3, COVERAGE, 81d, NCLOC, 10_000d)); + + ProjectMeasuresQuery query = new ProjectMeasuresQuery() + .addMetricCriterion(MetricCriterion.create(COVERAGE, Operator.EQ, 80d)); + + assertResults(query, PROJECT2); + } + + @Test + public void filter_on_no_data_with_several_projects() { + index( + newDoc(PROJECT1, NCLOC, 1d), + newDoc(PROJECT2, DUPLICATION, 80d)); + + ProjectMeasuresQuery query = new ProjectMeasuresQuery() + .addMetricCriterion(MetricCriterion.createNoData(DUPLICATION)); + + assertResults(query, PROJECT1); + } + + @Test + public void filter_on_no_data_should_not_return_projects_with_data_and_other_measures() { + ComponentDto project = ComponentTesting.newPrivateProjectDto(ORG); + index(newDoc(project, DUPLICATION, 80d, NCLOC, 1d)); + + ProjectMeasuresQuery query = new ProjectMeasuresQuery().addMetricCriterion(MetricCriterion.createNoData(DUPLICATION)); + + assertNoResults(query); + } + + @Test + public void filter_on_no_data_should_not_return_projects_with_data() { + ComponentDto project = ComponentTesting.newPrivateProjectDto(ORG); + index(newDoc(project, DUPLICATION, 80d)); + + ProjectMeasuresQuery query = new ProjectMeasuresQuery().addMetricCriterion(MetricCriterion.createNoData(DUPLICATION)); + + assertNoResults(query); + } + + @Test + public void filter_on_no_data_should_return_projects_with_no_data() { + ComponentDto project = ComponentTesting.newPrivateProjectDto(ORG); + index(newDoc(project, NCLOC, 1d)); + + ProjectMeasuresQuery query = new ProjectMeasuresQuery().addMetricCriterion(MetricCriterion.createNoData(DUPLICATION)); + + assertResults(query, project); + } + + @Test + public void filter_on_several_metrics() { + index( + newDoc(PROJECT1, COVERAGE, 81d, NCLOC, 10_001d), + newDoc(PROJECT2, COVERAGE, 80d, NCLOC, 10_001d), + newDoc(PROJECT3, COVERAGE, 79d, NCLOC, 10_000d)); + + ProjectMeasuresQuery esQuery = new ProjectMeasuresQuery() + .addMetricCriterion(MetricCriterion.create(COVERAGE, Operator.LTE, 80d)) + .addMetricCriterion(MetricCriterion.create(NCLOC, Operator.GT, 10_000d)) + .addMetricCriterion(MetricCriterion.create(NCLOC, Operator.LT, 11_000d)); + assertResults(esQuery, PROJECT2); + } + + @Test + public void filter_on_quality_gate_status() { + index( + newDoc(PROJECT1).setQualityGateStatus(OK.name()), + newDoc(PROJECT2).setQualityGateStatus(OK.name()), + newDoc(PROJECT3).setQualityGateStatus(ERROR.name())); + + ProjectMeasuresQuery query = new ProjectMeasuresQuery().setQualityGateStatus(OK); + assertResults(query, PROJECT1, PROJECT2); + } + + @Test + public void filter_on_languages() { + ComponentDto project4 = ComponentTesting.newPrivateProjectDto(ORG).setUuid("Project-4").setName("Project 4").setDbKey("key-4"); + index( + newDoc(PROJECT1).setLanguages(singletonList("java")), + newDoc(PROJECT2).setLanguages(singletonList("xoo")), + newDoc(PROJECT3).setLanguages(singletonList("xoo")), + newDoc(project4).setLanguages(asList("<null>", "java", "xoo"))); + + assertResults(new ProjectMeasuresQuery().setLanguages(newHashSet("java", "xoo")), PROJECT1, PROJECT2, PROJECT3, project4); + assertResults(new ProjectMeasuresQuery().setLanguages(newHashSet("java")), PROJECT1, project4); + assertResults(new ProjectMeasuresQuery().setLanguages(newHashSet("unknown"))); + } + + @Test + public void filter_on_query_text() { + ComponentDto windows = ComponentTesting.newPrivateProjectDto(ORG).setUuid("windows").setName("Windows").setDbKey("project1"); + ComponentDto apachee = ComponentTesting.newPrivateProjectDto(ORG).setUuid("apachee").setName("apachee").setDbKey("project2"); + ComponentDto apache1 = ComponentTesting.newPrivateProjectDto(ORG).setUuid("apache-1").setName("Apache").setDbKey("project3"); + ComponentDto apache2 = ComponentTesting.newPrivateProjectDto(ORG).setUuid("apache-2").setName("Apache").setDbKey("project4"); + index(newDoc(windows), newDoc(apachee), newDoc(apache1), newDoc(apache2)); + + assertResults(new ProjectMeasuresQuery().setQueryText("windows"), windows); + assertResults(new ProjectMeasuresQuery().setQueryText("project2"), apachee); + assertResults(new ProjectMeasuresQuery().setQueryText("pAch"), apache1, apache2, apachee); + } + + @Test + public void filter_on_ids() { + index( + newDoc(PROJECT1), + newDoc(PROJECT2), + newDoc(PROJECT3)); + + ProjectMeasuresQuery query = new ProjectMeasuresQuery().setProjectUuids(newHashSet(PROJECT1.uuid(), PROJECT3.uuid())); + assertResults(query, PROJECT1, PROJECT3); + } + + @Test + public void filter_on_tags() { + index( + newDoc(PROJECT1).setTags(newArrayList("finance", "platform")), + newDoc(PROJECT2).setTags(newArrayList("marketing", "platform")), + newDoc(PROJECT3).setTags(newArrayList("finance", "language"))); + + assertResults(new ProjectMeasuresQuery().setTags(newHashSet("finance")), PROJECT1, PROJECT3); + assertResults(new ProjectMeasuresQuery().setTags(newHashSet("finance", "language")), PROJECT1, PROJECT3); + assertResults(new ProjectMeasuresQuery().setTags(newHashSet("finance", "marketing")), PROJECT1, PROJECT2, PROJECT3); + assertResults(new ProjectMeasuresQuery().setTags(newHashSet("marketing")), PROJECT2); + assertNoResults(new ProjectMeasuresQuery().setTags(newHashSet("tag 42"))); + } + + @Test + public void filter_on_organization() { + OrganizationDto org1 = OrganizationTesting.newOrganizationDto(); + OrganizationDto org2 = OrganizationTesting.newOrganizationDto(); + ComponentDto projectInOrg1 = ComponentTesting.newPrivateProjectDto(org1); + ComponentDto projectInOrg2 = ComponentTesting.newPrivateProjectDto(org2); + index(newDoc(projectInOrg1), newDoc(projectInOrg2)); + + ProjectMeasuresQuery query1 = new ProjectMeasuresQuery().setOrganizationUuid(org1.getUuid()); + assertResults(query1, projectInOrg1); + + ProjectMeasuresQuery query2 = new ProjectMeasuresQuery().setOrganizationUuid(org2.getUuid()); + assertResults(query2, projectInOrg2); + + ProjectMeasuresQuery query3 = new ProjectMeasuresQuery().setOrganizationUuid("another_org"); + assertNoResults(query3); + } + + @Test + public void return_only_projects_authorized_for_user() { + indexForUser(USER1, newDoc(PROJECT1), newDoc(PROJECT2)); + indexForUser(USER2, newDoc(PROJECT3)); + + userSession.logIn(USER1); + assertResults(new ProjectMeasuresQuery(), PROJECT1, PROJECT2); + } + + @Test + public void return_only_projects_authorized_for_user_groups() { + indexForGroup(GROUP1, newDoc(PROJECT1), newDoc(PROJECT2)); + indexForGroup(GROUP2, newDoc(PROJECT3)); + + userSession.logIn().setGroups(GROUP1); + assertResults(new ProjectMeasuresQuery(), PROJECT1, PROJECT2); + } + + @Test + public void return_only_projects_authorized_for_user_and_groups() { + indexForUser(USER1, newDoc(PROJECT1), newDoc(PROJECT2)); + indexForGroup(GROUP1, newDoc(PROJECT3)); + + userSession.logIn(USER1).setGroups(GROUP1); + assertResults(new ProjectMeasuresQuery(), PROJECT1, PROJECT2, PROJECT3); + } + + @Test + public void anonymous_user_can_only_access_projects_authorized_for_anyone() { + index(newDoc(PROJECT1)); + indexForUser(USER1, newDoc(PROJECT2)); + + userSession.anonymous(); + assertResults(new ProjectMeasuresQuery(), PROJECT1); + } + + @Test + public void root_user_can_access_all_projects() { + indexForUser(USER1, newDoc(PROJECT1)); + // connecting with a root but not USER1 + userSession.logIn().setRoot(); + + assertResults(new ProjectMeasuresQuery(), PROJECT1); + } + + @Test + public void return_all_projects_when_setIgnoreAuthorization_is_true() { + indexForUser(USER1, newDoc(PROJECT1), newDoc(PROJECT2)); + indexForUser(USER2, newDoc(PROJECT3)); + userSession.logIn(USER1); + + assertResults(new ProjectMeasuresQuery().setIgnoreAuthorization(false), PROJECT1, PROJECT2); + assertResults(new ProjectMeasuresQuery().setIgnoreAuthorization(true), PROJECT1, PROJECT2, PROJECT3); + } + + @Test + public void does_not_return_facet_when_no_facets_in_options() { + index( + newDoc(PROJECT1, NCLOC, 10d, COVERAGE_KEY, 30d, MAINTAINABILITY_RATING, 3d) + .setQualityGateStatus(OK.name())); + + Facets facets = underTest.search(new ProjectMeasuresQuery(), new SearchOptions()).getFacets(); + + assertThat(facets.getAll()).isEmpty(); + } + + @Test + public void facet_ncloc() { + index( + // 3 docs with ncloc<1K + newDoc(NCLOC, 0d), + newDoc(NCLOC, 0d), + newDoc(NCLOC, 999d), + // 2 docs with ncloc>=1K and ncloc<10K + newDoc(NCLOC, 1_000d), + newDoc(NCLOC, 9_999d), + // 4 docs with ncloc>=10K and ncloc<100K + newDoc(NCLOC, 10_000d), + newDoc(NCLOC, 10_000d), + newDoc(NCLOC, 11_000d), + newDoc(NCLOC, 99_000d), + // 2 docs with ncloc>=100K and ncloc<500K + newDoc(NCLOC, 100_000d), + newDoc(NCLOC, 499_000d), + // 5 docs with ncloc>= 500K + newDoc(NCLOC, 500_000d), + newDoc(NCLOC, 100_000_000d), + newDoc(NCLOC, 500_000d), + newDoc(NCLOC, 1_000_000d), + newDoc(NCLOC, 100_000_000_000d)); + + Facets facets = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(NCLOC)).getFacets(); + + assertThat(facets.get(NCLOC)).containsExactly( + entry("*-1000.0", 3L), + entry("1000.0-10000.0", 2L), + entry("10000.0-100000.0", 4L), + entry("100000.0-500000.0", 2L), + entry("500000.0-*", 5L)); + } + + @Test + public void facet_ncloc_is_sticky() { + index( + // 1 docs with ncloc<1K + newDoc(NCLOC, 999d, COVERAGE, 0d, DUPLICATION, 0d), + // 2 docs with ncloc>=1K and ncloc<10K + newDoc(NCLOC, 1_000d, COVERAGE, 10d, DUPLICATION, 0d), + newDoc(NCLOC, 9_999d, COVERAGE, 20d, DUPLICATION, 0d), + // 3 docs with ncloc>=10K and ncloc<100K + newDoc(NCLOC, 10_000d, COVERAGE, 31d, DUPLICATION, 0d), + newDoc(NCLOC, 11_000d, COVERAGE, 40d, DUPLICATION, 0d), + newDoc(NCLOC, 99_000d, COVERAGE, 50d, DUPLICATION, 0d), + // 2 docs with ncloc>=100K and ncloc<500K + newDoc(NCLOC, 100_000d, COVERAGE, 71d, DUPLICATION, 0d), + newDoc(NCLOC, 499_000d, COVERAGE, 80d, DUPLICATION, 0d), + // 1 docs with ncloc>= 500K + newDoc(NCLOC, 501_000d, COVERAGE, 81d, DUPLICATION, 20d)); + + Facets facets = underTest.search(new ProjectMeasuresQuery() + .addMetricCriterion(MetricCriterion.create(NCLOC, Operator.LT, 10_000d)) + .addMetricCriterion(MetricCriterion.create(DUPLICATION, Operator.LT, 10d)), + new SearchOptions().addFacets(NCLOC, COVERAGE)).getFacets(); + + // Sticky facet on ncloc does not take into account ncloc filter + assertThat(facets.get(NCLOC)).containsExactly( + entry("*-1000.0", 1L), + entry("1000.0-10000.0", 2L), + entry("10000.0-100000.0", 3L), + entry("100000.0-500000.0", 2L), + entry("500000.0-*", 0L)); + // But facet on coverage does well take into into filters + assertThat(facets.get(COVERAGE)).containsOnly( + entry("NO_DATA", 0L), + entry("*-30.0", 3L), + entry("30.0-50.0", 0L), + entry("50.0-70.0", 0L), + entry("70.0-80.0", 0L), + entry("80.0-*", 0L)); + } + + @Test + public void facet_ncloc_contains_only_projects_authorized_for_user() { + // User can see these projects + indexForUser(USER1, + // docs with ncloc<1K + newDoc(NCLOC, 0d), + newDoc(NCLOC, 100d), + newDoc(NCLOC, 999d), + // docs with ncloc>=1K and ncloc<10K + newDoc(NCLOC, 1_000d), + newDoc(NCLOC, 9_999d)); + + // User cannot see these projects + indexForUser(USER2, + // doc with ncloc>=10K and ncloc<100K + newDoc(NCLOC, 11_000d), + // doc with ncloc>=100K and ncloc<500K + newDoc(NCLOC, 499_000d), + // doc with ncloc>= 500K + newDoc(NCLOC, 501_000d)); + + userSession.logIn(USER1); + Facets facets = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(NCLOC)).getFacets(); + + assertThat(facets.get(NCLOC)).containsExactly( + entry("*-1000.0", 3L), + entry("1000.0-10000.0", 2L), + entry("10000.0-100000.0", 0L), + entry("100000.0-500000.0", 0L), + entry("500000.0-*", 0L)); + } + + @Test + public void facet_new_lines() { + index( + // 3 docs with ncloc<1K + newDoc(NEW_LINES, 0d), + newDoc(NEW_LINES, 0d), + newDoc(NEW_LINES, 999d), + // 2 docs with ncloc>=1K and ncloc<10K + newDoc(NEW_LINES, 1_000d), + newDoc(NEW_LINES, 9_999d), + // 4 docs with ncloc>=10K and ncloc<100K + newDoc(NEW_LINES, 10_000d), + newDoc(NEW_LINES, 10_000d), + newDoc(NEW_LINES, 11_000d), + newDoc(NEW_LINES, 99_000d), + // 2 docs with ncloc>=100K and ncloc<500K + newDoc(NEW_LINES, 100_000d), + newDoc(NEW_LINES, 499_000d), + // 5 docs with ncloc>= 500K + newDoc(NEW_LINES, 500_000d), + newDoc(NEW_LINES, 100_000_000d), + newDoc(NEW_LINES, 500_000d), + newDoc(NEW_LINES, 1_000_000d), + newDoc(NEW_LINES, 100_000_000_000d)); + + Facets facets = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(NEW_LINES)).getFacets(); + + assertThat(facets.get(NEW_LINES)).containsExactly( + entry("*-1000.0", 3L), + entry("1000.0-10000.0", 2L), + entry("10000.0-100000.0", 4L), + entry("100000.0-500000.0", 2L), + entry("500000.0-*", 5L)); + } + + @Test + public void facet_coverage() { + index( + // 1 doc with no coverage + newDocWithNoMeasure(), + // 3 docs with coverage<30% + newDoc(COVERAGE, 0d), + newDoc(COVERAGE, 0d), + newDoc(COVERAGE, 29d), + // 2 docs with coverage>=30% and coverage<50% + newDoc(COVERAGE, 30d), + newDoc(COVERAGE, 49d), + // 4 docs with coverage>=50% and coverage<70% + newDoc(COVERAGE, 50d), + newDoc(COVERAGE, 60d), + newDoc(COVERAGE, 60d), + newDoc(COVERAGE, 69d), + // 2 docs with coverage>=70% and coverage<80% + newDoc(COVERAGE, 70d), + newDoc(COVERAGE, 79d), + // 5 docs with coverage>= 80% + newDoc(COVERAGE, 80d), + newDoc(COVERAGE, 80d), + newDoc(COVERAGE, 90d), + newDoc(COVERAGE, 90.5d), + newDoc(COVERAGE, 100d)); + + Facets facets = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(COVERAGE)).getFacets(); + + assertThat(facets.get(COVERAGE)).containsOnly( + entry("NO_DATA", 1L), + entry("*-30.0", 3L), + entry("30.0-50.0", 2L), + entry("50.0-70.0", 4L), + entry("70.0-80.0", 2L), + entry("80.0-*", 5L)); + } + + @Test + public void facet_coverage_is_sticky() { + index( + // docs with no coverage + newDoc(NCLOC, 999d, DUPLICATION, 0d), + newDoc(NCLOC, 999d, DUPLICATION, 1d), + newDoc(NCLOC, 999d, DUPLICATION, 20d), + // docs with coverage<30% + newDoc(NCLOC, 999d, COVERAGE, 0d, DUPLICATION, 0d), + newDoc(NCLOC, 1_000d, COVERAGE, 10d, DUPLICATION, 0d), + newDoc(NCLOC, 9_999d, COVERAGE, 20d, DUPLICATION, 0d), + // docs with coverage>=30% and coverage<50% + newDoc(NCLOC, 10_000d, COVERAGE, 31d, DUPLICATION, 0d), + newDoc(NCLOC, 11_000d, COVERAGE, 40d, DUPLICATION, 0d), + // docs with coverage>=50% and coverage<70% + newDoc(NCLOC, 99_000d, COVERAGE, 50d, DUPLICATION, 0d), + // docs with coverage>=70% and coverage<80% + newDoc(NCLOC, 100_000d, COVERAGE, 71d, DUPLICATION, 0d), + // docs with coverage>= 80% + newDoc(NCLOC, 499_000d, COVERAGE, 80d, DUPLICATION, 15d), + newDoc(NCLOC, 501_000d, COVERAGE, 810d, DUPLICATION, 20d)); + + Facets facets = underTest.search(new ProjectMeasuresQuery() + .addMetricCriterion(MetricCriterion.create(COVERAGE, Operator.LT, 30d)) + .addMetricCriterion(MetricCriterion.create(DUPLICATION, Operator.LT, 10d)), + new SearchOptions().addFacets(COVERAGE, NCLOC)).getFacets(); + + // Sticky facet on coverage does not take into account coverage filter + assertThat(facets.get(COVERAGE)).containsExactly( + entry("NO_DATA", 2L), + entry("*-30.0", 3L), + entry("30.0-50.0", 2L), + entry("50.0-70.0", 1L), + entry("70.0-80.0", 1L), + entry("80.0-*", 0L)); + // But facet on ncloc does well take into into filters + assertThat(facets.get(NCLOC)).containsExactly( + entry("*-1000.0", 1L), + entry("1000.0-10000.0", 2L), + entry("10000.0-100000.0", 0L), + entry("100000.0-500000.0", 0L), + entry("500000.0-*", 0L)); + } + + @Test + public void facet_coverage_contains_only_projects_authorized_for_user() { + // User can see these projects + indexForUser(USER1, + // 1 doc with no coverage + newDocWithNoMeasure(), + // docs with coverage<30% + newDoc(COVERAGE, 0d), + newDoc(COVERAGE, 0d), + newDoc(COVERAGE, 29d), + // docs with coverage>=30% and coverage<50% + newDoc(COVERAGE, 30d), + newDoc(COVERAGE, 49d)); + + // User cannot see these projects + indexForUser(USER2, + // 2 docs with no coverage + newDocWithNoMeasure(), + newDocWithNoMeasure(), + // docs with coverage>=50% and coverage<70% + newDoc(COVERAGE, 50d), + // docs with coverage>=70% and coverage<80% + newDoc(COVERAGE, 70d), + // docs with coverage>= 80% + newDoc(COVERAGE, 80d)); + + userSession.logIn(USER1); + Facets facets = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(COVERAGE)).getFacets(); + + assertThat(facets.get(COVERAGE)).containsExactly( + entry("NO_DATA", 1L), + entry("*-30.0", 3L), + entry("30.0-50.0", 2L), + entry("50.0-70.0", 0L), + entry("70.0-80.0", 0L), + entry("80.0-*", 0L)); + } + + @Test + public void facet_new_coverage() { + index( + // 1 doc with no coverage + newDocWithNoMeasure(), + // 3 docs with coverage<30% + newDoc(NEW_COVERAGE, 0d), + newDoc(NEW_COVERAGE, 0d), + newDoc(NEW_COVERAGE, 29d), + // 2 docs with coverage>=30% and coverage<50% + newDoc(NEW_COVERAGE, 30d), + newDoc(NEW_COVERAGE, 49d), + // 4 docs with coverage>=50% and coverage<70% + newDoc(NEW_COVERAGE, 50d), + newDoc(NEW_COVERAGE, 60d), + newDoc(NEW_COVERAGE, 60d), + newDoc(NEW_COVERAGE, 69d), + // 2 docs with coverage>=70% and coverage<80% + newDoc(NEW_COVERAGE, 70d), + newDoc(NEW_COVERAGE, 79d), + // 5 docs with coverage>= 80% + newDoc(NEW_COVERAGE, 80d), + newDoc(NEW_COVERAGE, 80d), + newDoc(NEW_COVERAGE, 90d), + newDoc(NEW_COVERAGE, 90.5d), + newDoc(NEW_COVERAGE, 100d)); + + Facets facets = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(NEW_COVERAGE)).getFacets(); + + assertThat(facets.get(NEW_COVERAGE)).containsOnly( + entry("NO_DATA", 1L), + entry("*-30.0", 3L), + entry("30.0-50.0", 2L), + entry("50.0-70.0", 4L), + entry("70.0-80.0", 2L), + entry("80.0-*", 5L)); + } + + @Test + public void facet_duplicated_lines_density() { + index( + // 1 doc with no duplication + newDocWithNoMeasure(), + // 3 docs with duplication<3% + newDoc(DUPLICATION, 0d), + newDoc(DUPLICATION, 0d), + newDoc(DUPLICATION, 2.9d), + // 2 docs with duplication>=3% and duplication<5% + newDoc(DUPLICATION, 3d), + newDoc(DUPLICATION, 4.9d), + // 4 docs with duplication>=5% and duplication<10% + newDoc(DUPLICATION, 5d), + newDoc(DUPLICATION, 6d), + newDoc(DUPLICATION, 6d), + newDoc(DUPLICATION, 9.9d), + // 2 docs with duplication>=10% and duplication<20% + newDoc(DUPLICATION, 10d), + newDoc(DUPLICATION, 19.9d), + // 5 docs with duplication>= 20% + newDoc(DUPLICATION, 20d), + newDoc(DUPLICATION, 20d), + newDoc(DUPLICATION, 50d), + newDoc(DUPLICATION, 80d), + newDoc(DUPLICATION, 100d)); + + Facets facets = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(DUPLICATION)).getFacets(); + + assertThat(facets.get(DUPLICATION)).containsOnly( + entry("NO_DATA", 1L), + entry("*-3.0", 3L), + entry("3.0-5.0", 2L), + entry("5.0-10.0", 4L), + entry("10.0-20.0", 2L), + entry("20.0-*", 5L)); + } + + @Test + public void facet_duplicated_lines_density_is_sticky() { + index( + // docs with no duplication + newDoc(NCLOC, 50_001d, COVERAGE, 29d), + // docs with duplication<3% + newDoc(DUPLICATION, 0d, NCLOC, 999d, COVERAGE, 0d), + // docs with duplication>=3% and duplication<5% + newDoc(DUPLICATION, 3d, NCLOC, 5000d, COVERAGE, 0d), + newDoc(DUPLICATION, 4.9d, NCLOC, 6000d, COVERAGE, 0d), + // docs with duplication>=5% and duplication<10% + newDoc(DUPLICATION, 5d, NCLOC, 11000d, COVERAGE, 0d), + // docs with duplication>=10% and duplication<20% + newDoc(DUPLICATION, 10d, NCLOC, 120000d, COVERAGE, 10d), + newDoc(DUPLICATION, 19.9d, NCLOC, 130000d, COVERAGE, 20d), + // docs with duplication>= 20% + newDoc(DUPLICATION, 20d, NCLOC, 1000000d, COVERAGE, 40d)); + + Facets facets = underTest.search(new ProjectMeasuresQuery() + .addMetricCriterion(MetricCriterion.create(DUPLICATION, Operator.LT, 10d)) + .addMetricCriterion(MetricCriterion.create(COVERAGE, Operator.LT, 30d)), + new SearchOptions().addFacets(DUPLICATION, NCLOC)).getFacets(); + + // Sticky facet on duplication does not take into account duplication filter + assertThat(facets.get(DUPLICATION)).containsOnly( + entry("NO_DATA", 1L), + entry("*-3.0", 1L), + entry("3.0-5.0", 2L), + entry("5.0-10.0", 1L), + entry("10.0-20.0", 2L), + entry("20.0-*", 0L)); + // But facet on ncloc does well take into into filters + assertThat(facets.get(NCLOC)).containsOnly( + entry("*-1000.0", 1L), + entry("1000.0-10000.0", 2L), + entry("10000.0-100000.0", 1L), + entry("100000.0-500000.0", 0L), + entry("500000.0-*", 0L)); + } + + @Test + public void facet_duplicated_lines_density_contains_only_projects_authorized_for_user() { + // User can see these projects + indexForUser(USER1, + // docs with no duplication + newDocWithNoMeasure(), + // docs with duplication<3% + newDoc(DUPLICATION, 0d), + newDoc(DUPLICATION, 0d), + newDoc(DUPLICATION, 2.9d), + // docs with duplication>=3% and duplication<5% + newDoc(DUPLICATION, 3d), + newDoc(DUPLICATION, 4.9d)); + + // User cannot see these projects + indexForUser(USER2, + // docs with no duplication + newDocWithNoMeasure(), + newDocWithNoMeasure(), + // docs with duplication>=5% and duplication<10% + newDoc(DUPLICATION, 5d), + // docs with duplication>=10% and duplication<20% + newDoc(DUPLICATION, 10d), + // docs with duplication>= 20% + newDoc(DUPLICATION, 20d)); + + userSession.logIn(USER1); + Facets facets = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(DUPLICATION)).getFacets(); + + assertThat(facets.get(DUPLICATION)).containsOnly( + entry("NO_DATA", 1L), + entry("*-3.0", 3L), + entry("3.0-5.0", 2L), + entry("5.0-10.0", 0L), + entry("10.0-20.0", 0L), + entry("20.0-*", 0L)); + } + + @Test + public void facet_new_duplicated_lines_density() { + index( + // 2 docs with no measure + newDocWithNoMeasure(), + newDocWithNoMeasure(), + // 3 docs with duplication<3% + newDoc(NEW_DUPLICATION, 0d), + newDoc(NEW_DUPLICATION, 0d), + newDoc(NEW_DUPLICATION, 2.9d), + // 2 docs with duplication>=3% and duplication<5% + newDoc(NEW_DUPLICATION, 3d), + newDoc(NEW_DUPLICATION, 4.9d), + // 4 docs with duplication>=5% and duplication<10% + newDoc(NEW_DUPLICATION, 5d), + newDoc(NEW_DUPLICATION, 6d), + newDoc(NEW_DUPLICATION, 6d), + newDoc(NEW_DUPLICATION, 9.9d), + // 2 docs with duplication>=10% and duplication<20% + newDoc(NEW_DUPLICATION, 10d), + newDoc(NEW_DUPLICATION, 19.9d), + // 5 docs with duplication>= 20% + newDoc(NEW_DUPLICATION, 20d), + newDoc(NEW_DUPLICATION, 20d), + newDoc(NEW_DUPLICATION, 50d), + newDoc(NEW_DUPLICATION, 80d), + newDoc(NEW_DUPLICATION, 100d)); + + Facets facets = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(NEW_DUPLICATION)).getFacets(); + + assertThat(facets.get(NEW_DUPLICATION)).containsExactly( + entry("NO_DATA", 2L), + entry("*-3.0", 3L), + entry("3.0-5.0", 2L), + entry("5.0-10.0", 4L), + entry("10.0-20.0", 2L), + entry("20.0-*", 5L)); + } + + @Test + @UseDataProvider("rating_metric_keys") + public void facet_on_rating(String metricKey) { + index( + // 3 docs with rating A + newDoc(metricKey, 1d), + newDoc(metricKey, 1d), + newDoc(metricKey, 1d), + // 2 docs with rating B + newDoc(metricKey, 2d), + newDoc(metricKey, 2d), + // 4 docs with rating C + newDoc(metricKey, 3d), + newDoc(metricKey, 3d), + newDoc(metricKey, 3d), + newDoc(metricKey, 3d), + // 2 docs with rating D + newDoc(metricKey, 4d), + newDoc(metricKey, 4d), + // 5 docs with rating E + newDoc(metricKey, 5d), + newDoc(metricKey, 5d), + newDoc(metricKey, 5d), + newDoc(metricKey, 5d), + newDoc(metricKey, 5d)); + + Facets facets = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(metricKey)).getFacets(); + + assertThat(facets.get(metricKey)).containsExactly( + entry("1", 3L), + entry("2", 2L), + entry("3", 4L), + entry("4", 2L), + entry("5", 5L)); + } + + @Test + @UseDataProvider("rating_metric_keys") + public void facet_on_rating_is_sticky(String metricKey) { + index( + // docs with rating A + newDoc(metricKey, 1d, NCLOC, 100d, COVERAGE, 0d), + newDoc(metricKey, 1d, NCLOC, 200d, COVERAGE, 0d), + newDoc(metricKey, 1d, NCLOC, 999d, COVERAGE, 0d), + // docs with rating B + newDoc(metricKey, 2d, NCLOC, 2000d, COVERAGE, 0d), + newDoc(metricKey, 2d, NCLOC, 5000d, COVERAGE, 0d), + // docs with rating C + newDoc(metricKey, 3d, NCLOC, 20000d, COVERAGE, 0d), + newDoc(metricKey, 3d, NCLOC, 30000d, COVERAGE, 0d), + newDoc(metricKey, 3d, NCLOC, 40000d, COVERAGE, 0d), + newDoc(metricKey, 3d, NCLOC, 50000d, COVERAGE, 0d), + // docs with rating D + newDoc(metricKey, 4d, NCLOC, 120000d, COVERAGE, 0d), + // docs with rating E + newDoc(metricKey, 5d, NCLOC, 600000d, COVERAGE, 40d), + newDoc(metricKey, 5d, NCLOC, 700000d, COVERAGE, 50d), + newDoc(metricKey, 5d, NCLOC, 800000d, COVERAGE, 60d)); + + Facets facets = underTest.search(new ProjectMeasuresQuery() + .addMetricCriterion(MetricCriterion.create(metricKey, Operator.LT, 3d)) + .addMetricCriterion(MetricCriterion.create(COVERAGE, Operator.LT, 30d)), + new SearchOptions().addFacets(metricKey, NCLOC)).getFacets(); + + // Sticky facet on maintainability rating does not take into account maintainability rating filter + assertThat(facets.get(metricKey)).containsExactly( + entry("1", 3L), + entry("2", 2L), + entry("3", 4L), + entry("4", 1L), + entry("5", 0L)); + // But facet on ncloc does well take into into filters + assertThat(facets.get(NCLOC)).containsExactly( + entry("*-1000.0", 3L), + entry("1000.0-10000.0", 2L), + entry("10000.0-100000.0", 0L), + entry("100000.0-500000.0", 0L), + entry("500000.0-*", 0L)); + } + + @Test + @UseDataProvider("rating_metric_keys") + public void facet_on_rating_contains_only_projects_authorized_for_user(String metricKey) { + // User can see these projects + indexForUser(USER1, + // 3 docs with rating A + newDoc(metricKey, 1d), + newDoc(metricKey, 1d), + newDoc(metricKey, 1d), + // 2 docs with rating B + newDoc(metricKey, 2d), + newDoc(metricKey, 2d)); + + // User cannot see these projects + indexForUser(USER2, + // docs with rating C + newDoc(metricKey, 3d), + // docs with rating D + newDoc(metricKey, 4d), + // docs with rating E + newDoc(metricKey, 5d)); + + userSession.logIn(USER1); + Facets facets = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(metricKey)).getFacets(); + + assertThat(facets.get(metricKey)).containsExactly( + entry("1", 3L), + entry("2", 2L), + entry("3", 0L), + entry("4", 0L), + entry("5", 0L)); + } + + @Test + public void facet_quality_gate() { + index( + // 2 docs with QG OK + newDoc().setQualityGateStatus(OK.name()), + newDoc().setQualityGateStatus(OK.name()), + // 4 docs with QG ERROR + newDoc().setQualityGateStatus(ERROR.name()), + newDoc().setQualityGateStatus(ERROR.name()), + newDoc().setQualityGateStatus(ERROR.name()), + newDoc().setQualityGateStatus(ERROR.name())); + + LinkedHashMap<String, Long> result = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(ALERT_STATUS_KEY)).getFacets().get(ALERT_STATUS_KEY); + + assertThat(result).containsOnly( + entry(ERROR.name(), 4L), + entry(OK.name(), 2L), + entry(WARN.name(), 0L)); + } + + @Test + public void facet_quality_gate_is_sticky() { + index( + // 2 docs with QG OK + newDoc(NCLOC, 10d, COVERAGE, 0d).setQualityGateStatus(OK.name()), + newDoc(NCLOC, 10d, COVERAGE, 0d).setQualityGateStatus(OK.name()), + // 4 docs with QG ERROR + newDoc(NCLOC, 100d, COVERAGE, 0d).setQualityGateStatus(ERROR.name()), + newDoc(NCLOC, 5000d, COVERAGE, 40d).setQualityGateStatus(ERROR.name()), + newDoc(NCLOC, 12000d, COVERAGE, 50d).setQualityGateStatus(ERROR.name()), + newDoc(NCLOC, 13000d, COVERAGE, 60d).setQualityGateStatus(ERROR.name())); + + Facets facets = underTest.search(new ProjectMeasuresQuery() + .setQualityGateStatus(ERROR) + .addMetricCriterion(MetricCriterion.create(COVERAGE, Operator.LT, 55d)), + new SearchOptions().addFacets(ALERT_STATUS_KEY, NCLOC)).getFacets(); + + // Sticky facet on quality gate does not take into account quality gate filter + assertThat(facets.get(ALERT_STATUS_KEY)).containsOnly( + entry(OK.name(), 2L), + entry(ERROR.name(), 3L), + entry(WARN.name(), 0L)); + // But facet on ncloc does well take into into filters + assertThat(facets.get(NCLOC)).containsExactly( + entry("*-1000.0", 1L), + entry("1000.0-10000.0", 1L), + entry("10000.0-100000.0", 1L), + entry("100000.0-500000.0", 0L), + entry("500000.0-*", 0L)); + } + + @Test + public void facet_quality_gate_contains_only_projects_authorized_for_user() { + // User can see these projects + indexForUser(USER1, + // 2 docs with QG OK + newDoc().setQualityGateStatus(OK.name()), + newDoc().setQualityGateStatus(OK.name())); + + // User cannot see these projects + indexForUser(USER2, + // 4 docs with QG ERROR + newDoc().setQualityGateStatus(ERROR.name()), + newDoc().setQualityGateStatus(ERROR.name()), + newDoc().setQualityGateStatus(ERROR.name()), + newDoc().setQualityGateStatus(ERROR.name())); + + userSession.logIn(USER1); + LinkedHashMap<String, Long> result = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(ALERT_STATUS_KEY)).getFacets().get(ALERT_STATUS_KEY); + + assertThat(result).containsOnly( + entry(ERROR.name(), 0L), + entry(OK.name(), 2L), + entry(WARN.name(), 0L)); + } + + @Test + public void facet_quality_gate_using_deprecated_warning() { + index( + // 2 docs with QG OK + newDoc().setQualityGateStatus(OK.name()), + newDoc().setQualityGateStatus(OK.name()), + // 3 docs with QG WARN + newDoc().setQualityGateStatus(WARN.name()), + newDoc().setQualityGateStatus(WARN.name()), + newDoc().setQualityGateStatus(WARN.name()), + // 4 docs with QG ERROR + newDoc().setQualityGateStatus(ERROR.name()), + newDoc().setQualityGateStatus(ERROR.name()), + newDoc().setQualityGateStatus(ERROR.name()), + newDoc().setQualityGateStatus(ERROR.name())); + + LinkedHashMap<String, Long> result = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(ALERT_STATUS_KEY)).getFacets().get(ALERT_STATUS_KEY); + + assertThat(result).containsOnly( + entry(ERROR.name(), 4L), + entry(WARN.name(), 3L), + entry(OK.name(), 2L)); + } + + @Test + public void facet_quality_gate_does_not_return_deprecated_warning_when_set_ignore_warning_is_true() { + index( + // 2 docs with QG OK + newDoc().setQualityGateStatus(OK.name()), + newDoc().setQualityGateStatus(OK.name()), + // 4 docs with QG ERROR + newDoc().setQualityGateStatus(ERROR.name()), + newDoc().setQualityGateStatus(ERROR.name()), + newDoc().setQualityGateStatus(ERROR.name()), + newDoc().setQualityGateStatus(ERROR.name())); + + assertThat(underTest.search(new ProjectMeasuresQuery().setIgnoreWarning(true), new SearchOptions().addFacets(ALERT_STATUS_KEY)).getFacets().get(ALERT_STATUS_KEY)).containsOnly( + entry(ERROR.name(), 4L), + entry(OK.name(), 2L)); + assertThat(underTest.search(new ProjectMeasuresQuery().setIgnoreWarning(false), new SearchOptions().addFacets(ALERT_STATUS_KEY)).getFacets().get(ALERT_STATUS_KEY)).containsOnly( + entry(ERROR.name(), 4L), + entry(WARN.name(), 0L), + entry(OK.name(), 2L)); + } + + @Test + public void facet_languages() { + index( + newDoc().setLanguages(singletonList("java")), + newDoc().setLanguages(singletonList("java")), + newDoc().setLanguages(singletonList("xoo")), + newDoc().setLanguages(singletonList("xml")), + newDoc().setLanguages(asList("<null>", "java")), + newDoc().setLanguages(asList("<null>", "java", "xoo"))); + + Facets facets = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(LANGUAGES)).getFacets(); + + assertThat(facets.get(LANGUAGES)).containsOnly( + entry("<null>", 2L), + entry("java", 4L), + entry("xoo", 2L), + entry("xml", 1L)); + } + + @Test + public void facet_languages_is_limited_to_10_languages() { + index( + newDoc().setLanguages(asList("<null>", "java", "xoo", "css", "cpp")), + newDoc().setLanguages(asList("xml", "php", "python", "perl", "ruby")), + newDoc().setLanguages(asList("js", "scala"))); + + Facets facets = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(LANGUAGES)).getFacets(); + + assertThat(facets.get(LANGUAGES)).hasSize(10); + } + + @Test + public void facet_languages_is_sticky() { + index( + newDoc(NCLOC, 10d).setLanguages(singletonList("java")), + newDoc(NCLOC, 10d).setLanguages(singletonList("java")), + newDoc(NCLOC, 10d).setLanguages(singletonList("xoo")), + newDoc(NCLOC, 100d).setLanguages(singletonList("xml")), + newDoc(NCLOC, 100d).setLanguages(asList("<null>", "java")), + newDoc(NCLOC, 5000d).setLanguages(asList("<null>", "java", "xoo"))); + + Facets facets = underTest.search( + new ProjectMeasuresQuery().setLanguages(ImmutableSet.of("java")), + new SearchOptions().addFacets(LANGUAGES, NCLOC)).getFacets(); + + // Sticky facet on language does not take into account language filter + assertThat(facets.get(LANGUAGES)).containsOnly( + entry("<null>", 2L), + entry("java", 4L), + entry("xoo", 2L), + entry("xml", 1L)); + // But facet on ncloc does well take account into filters + assertThat(facets.get(NCLOC)).containsExactly( + entry("*-1000.0", 3L), + entry("1000.0-10000.0", 1L), + entry("10000.0-100000.0", 0L), + entry("100000.0-500000.0", 0L), + entry("500000.0-*", 0L)); + } + + @Test + public void facet_languages_returns_more_than_10_languages_when_languages_filter_contains_value_not_in_top_10() { + index( + newDoc().setLanguages(asList("<null>", "java", "xoo", "css", "cpp")), + newDoc().setLanguages(asList("xml", "php", "python", "perl", "ruby")), + newDoc().setLanguages(asList("js", "scala"))); + + Facets facets = underTest.search(new ProjectMeasuresQuery().setLanguages(ImmutableSet.of("xoo", "xml")), new SearchOptions().addFacets(LANGUAGES)).getFacets(); + + assertThat(facets.get(LANGUAGES)).containsOnly( + entry("<null>", 1L), + entry("cpp", 1L), + entry("css", 1L), + entry("java", 1L), + entry("js", 1L), + entry("perl", 1L), + entry("php", 1L), + entry("python", 1L), + entry("ruby", 1L), + entry("scala", 1L), + entry("xoo", 1L), + entry("xml", 1L)); + } + + @Test + public void facet_languages_contains_only_projects_authorized_for_user() { + // User can see these projects + indexForUser(USER1, + newDoc().setLanguages(singletonList("java")), + newDoc().setLanguages(asList("java", "xoo"))); + + // User cannot see these projects + indexForUser(USER2, + newDoc().setLanguages(singletonList("java")), + newDoc().setLanguages(asList("java", "xoo"))); + + userSession.logIn(USER1); + LinkedHashMap<String, Long> result = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(LANGUAGES)).getFacets().get(LANGUAGES); + + assertThat(result).containsOnly( + entry("java", 2L), + entry("xoo", 1L)); + } + + @Test + public void facet_tags() { + index( + newDoc().setTags(newArrayList("finance", "offshore", "java")), + newDoc().setTags(newArrayList("finance", "javascript")), + newDoc().setTags(newArrayList("marketing", "finance")), + newDoc().setTags(newArrayList("marketing", "offshore")), + newDoc().setTags(newArrayList("finance", "marketing")), + newDoc().setTags(newArrayList("finance"))); + + Map<String, Long> result = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(FIELD_TAGS)).getFacets().get(FIELD_TAGS); + + assertThat(result).containsOnly( + entry("finance", 5L), + entry("marketing", 3L), + entry("offshore", 2L), + entry("java", 1L), + entry("javascript", 1L)); + } + + @Test + public void facet_tags_is_sticky() { + index( + newDoc().setTags(newArrayList("finance")).setQualityGateStatus(OK.name()), + newDoc().setTags(newArrayList("finance")).setQualityGateStatus(ERROR.name()), + newDoc().setTags(newArrayList("cpp")).setQualityGateStatus(ERROR.name())); + + Facets facets = underTest.search( + new ProjectMeasuresQuery().setTags(newHashSet("cpp")), + new SearchOptions().addFacets(FIELD_TAGS).addFacets(ALERT_STATUS_KEY)) + .getFacets(); + + assertThat(facets.get(FIELD_TAGS)).containsOnly( + entry("finance", 2L), + entry("cpp", 1L)); + assertThat(facets.get(ALERT_STATUS_KEY)).containsOnly( + entry(OK.name(), 0L), + entry(ERROR.name(), 1L), + entry(WARN.name(), 0L)); + } + + @Test + public void facet_tags_returns_10_elements_by_default() { + index( + newDoc().setTags(newArrayList("finance1", "finance2", "finance3", "finance4", "finance5", "finance6", "finance7", "finance8", "finance9", "finance10")), + newDoc().setTags(newArrayList("finance1", "finance2", "finance3", "finance4", "finance5", "finance6", "finance7", "finance8", "finance9", "finance10")), + newDoc().setTags(newArrayList("solo"))); + + Map<String, Long> result = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(FIELD_TAGS)).getFacets().get(FIELD_TAGS); + + assertThat(result).hasSize(10).containsOnlyKeys("finance1", "finance2", "finance3", "finance4", "finance5", "finance6", "finance7", "finance8", "finance9", "finance10"); + } + + @Test + public void facet_tags_returns_more_than_10_tags_when_tags_filter_contains_value_not_in_top_10() { + index( + newDoc().setTags(newArrayList("finance1", "finance2", "finance3", "finance4", "finance5", "finance6", "finance7", "finance8", "finance9", "finance10")), + newDoc().setTags(newArrayList("finance1", "finance2", "finance3", "finance4", "finance5", "finance6", "finance7", "finance8", "finance9", "finance10")), + newDoc().setTags(newArrayList("solo", "solo2"))); + + Map<String, Long> result = underTest.search(new ProjectMeasuresQuery().setTags(ImmutableSet.of("solo", "solo2")), new SearchOptions().addFacets(FIELD_TAGS)).getFacets() + .get(FIELD_TAGS); + + assertThat(result).hasSize(12).containsOnlyKeys("finance1", "finance2", "finance3", "finance4", "finance5", "finance6", "finance7", "finance8", "finance9", "finance10", "solo", + "solo2"); + } + + @Test + public void search_tags() { + index( + newDoc().setTags(newArrayList("finance", "offshore", "java")), + newDoc().setTags(newArrayList("official", "javascript")), + newDoc().setTags(newArrayList("marketing", "official")), + newDoc().setTags(newArrayList("marketing", "Madhoff")), + newDoc().setTags(newArrayList("finance", "offshore")), + newDoc().setTags(newArrayList("offshore"))); + + List<String> result = underTest.searchTags("off", 10); + + assertThat(result).containsOnly("offshore", "official", "Madhoff"); + } + + @Test + public void search_tags_return_all_tags() { + index( + newDoc().setTags(newArrayList("finance", "offshore", "java")), + newDoc().setTags(newArrayList("official", "javascript")), + newDoc().setTags(newArrayList("marketing", "official")), + newDoc().setTags(newArrayList("marketing", "Madhoff")), + newDoc().setTags(newArrayList("finance", "offshore")), + newDoc().setTags(newArrayList("offshore"))); + + List<String> result = underTest.searchTags(null, 10); + + assertThat(result).containsOnly("offshore", "official", "Madhoff", "finance", "marketing", "java", "javascript"); + } + + @Test + public void search_tags_in_lexical_order() { + index( + newDoc().setTags(newArrayList("finance", "offshore", "java")), + newDoc().setTags(newArrayList("official", "javascript")), + newDoc().setTags(newArrayList("marketing", "official")), + newDoc().setTags(newArrayList("marketing", "Madhoff")), + newDoc().setTags(newArrayList("finance", "offshore")), + newDoc().setTags(newArrayList("offshore"))); + + List<String> result = underTest.searchTags(null, 10); + + assertThat(result).containsExactly("Madhoff", "finance", "java", "javascript", "marketing", "official", "offshore"); + } + + @Test + public void search_tags_only_of_authorized_projects() { + indexForUser(USER1, + newDoc(PROJECT1).setTags(singletonList("finance")), + newDoc(PROJECT2).setTags(singletonList("marketing"))); + indexForUser(USER2, + newDoc(PROJECT3).setTags(singletonList("offshore"))); + + userSession.logIn(USER1); + + List<String> result = underTest.searchTags(null, 10); + + assertThat(result).containsOnly("finance", "marketing"); + } + + @Test + public void search_tags_with_no_tags() { + List<String> result = underTest.searchTags("whatever", 10); + + assertThat(result).isEmpty(); + } + + @Test + public void search_tags_with_page_size_at_0() { + index(newDoc().setTags(newArrayList("offshore"))); + + List<String> result = underTest.searchTags(null, 0); + + assertThat(result).isEmpty(); + } + + @Test + public void search_statistics() { + es.putDocuments(TYPE_PROJECT_MEASURES, + newDoc("lines", 10, "coverage", 80) + .setLanguages(Arrays.asList("java", "cs", "js")) + .setNclocLanguageDistributionFromMap(ImmutableMap.of("java", 200, "cs", 250, "js", 50)), + newDoc("lines", 20, "coverage", 80) + .setLanguages(Arrays.asList("java", "python", "kotlin")) + .setNclocLanguageDistributionFromMap(ImmutableMap.of("java", 300, "python", 100, "kotlin", 404))); + + ProjectMeasuresStatistics result = underTest.searchTelemetryStatistics(); + + assertThat(result.getProjectCount()).isEqualTo(2); + assertThat(result.getProjectCountByLanguage()).containsOnly( + entry("java", 2L), entry("cs", 1L), entry("js", 1L), entry("python", 1L), entry("kotlin", 1L)); + assertThat(result.getNclocByLanguage()).containsOnly( + entry("java", 500L), entry("cs", 250L), entry("js", 50L), entry("python", 100L), entry("kotlin", 404L)); + } + + @Test + public void fail_if_page_size_greater_than_500() { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Page size must be lower than or equals to 500"); + + underTest.searchTags("whatever", 501); + } + + private void index(ProjectMeasuresDoc... docs) { + es.putDocuments(TYPE_PROJECT_MEASURES, docs); + authorizationIndexer.allow(stream(docs).map(doc -> new IndexPermissions(doc.getId(), PROJECT).allowAnyone()).collect(toList())); + } + + private void indexForUser(UserDto user, ProjectMeasuresDoc... docs) { + es.putDocuments(TYPE_PROJECT_MEASURES, docs); + authorizationIndexer.allow(stream(docs).map(doc -> new IndexPermissions(doc.getId(), PROJECT).addUserId(user.getId())).collect(toList())); + } + + private void indexForGroup(GroupDto group, ProjectMeasuresDoc... docs) { + es.putDocuments(TYPE_PROJECT_MEASURES, docs); + authorizationIndexer.allow(stream(docs).map(doc -> new IndexPermissions(doc.getId(), PROJECT).addGroupId(group.getId())).collect(toList())); + } + + private static ProjectMeasuresDoc newDoc(ComponentDto project) { + return new ProjectMeasuresDoc() + .setOrganizationUuid(project.getOrganizationUuid()) + .setId(project.uuid()) + .setKey(project.getDbKey()) + .setName(project.name()); + } + + private static ProjectMeasuresDoc newDoc() { + return newDoc(ComponentTesting.newPrivateProjectDto(ORG)); + } + + private static ProjectMeasuresDoc newDoc(ComponentDto project, String metric1, Object value1) { + return newDoc(project).setMeasures(newArrayList(newMeasure(metric1, value1))); + } + + private static ProjectMeasuresDoc newDoc(ComponentDto project, String metric1, Object value1, String metric2, Object value2) { + return newDoc(project).setMeasures(newArrayList(newMeasure(metric1, value1), newMeasure(metric2, value2))); + } + + private static ProjectMeasuresDoc newDoc(ComponentDto project, String metric1, Object value1, String metric2, Object value2, String metric3, Object value3) { + return newDoc(project).setMeasures(newArrayList(newMeasure(metric1, value1), newMeasure(metric2, value2), newMeasure(metric3, value3))); + } + + private static Map<String, Object> newMeasure(String key, Object value) { + return ImmutableMap.of("key", key, "value", value); + } + + private static ProjectMeasuresDoc newDocWithNoMeasure() { + return newDoc(ComponentTesting.newPrivateProjectDto(ORG)); + } + + private static ProjectMeasuresDoc newDoc(String metric1, Object value1) { + return newDoc(ComponentTesting.newPrivateProjectDto(ORG), metric1, value1); + } + + private static ProjectMeasuresDoc newDoc(String metric1, Object value1, String metric2, Object value2) { + return newDoc(ComponentTesting.newPrivateProjectDto(ORG), metric1, value1, metric2, value2); + } + + private static ProjectMeasuresDoc newDoc(String metric1, Object value1, String metric2, Object value2, String metric3, Object value3) { + return newDoc(ComponentTesting.newPrivateProjectDto(ORG), metric1, value1, metric2, value2, metric3, value3); + } + + private void assertResults(ProjectMeasuresQuery query, ComponentDto... expectedProjects) { + List<String> result = underTest.search(query, new SearchOptions()).getIds(); + assertThat(result).containsExactly(Arrays.stream(expectedProjects).map(ComponentDto::uuid).toArray(String[]::new)); + } + + private void assertNoResults(ProjectMeasuresQuery query) { + assertResults(query); + } +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexTextSearchTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexTextSearchTest.java new file mode 100644 index 00000000000..344b598c5c7 --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexTextSearchTest.java @@ -0,0 +1,333 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.measure.index; + +import com.google.common.collect.ImmutableMap; +import java.util.List; +import java.util.Map; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.utils.System2; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.organization.OrganizationDto; +import org.sonar.db.organization.OrganizationTesting; +import org.sonar.server.es.EsTester; +import org.sonar.server.es.Facets; +import org.sonar.server.es.SearchOptions; +import org.sonar.server.measure.index.ProjectMeasuresQuery.MetricCriterion; +import org.sonar.server.permission.index.IndexPermissions; +import org.sonar.server.permission.index.PermissionIndexerTester; +import org.sonar.server.permission.index.WebAuthorizationTypeSupport; +import org.sonar.server.tester.UserSessionRule; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.collect.Lists.newArrayList; +import static java.util.Arrays.stream; +import static java.util.stream.Collectors.toList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; +import static org.sonar.api.resources.Qualifiers.PROJECT; +import static org.sonar.db.component.ComponentTesting.newPrivateProjectDto; +import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.TYPE_PROJECT_MEASURES; +import static org.sonar.server.measure.index.ProjectMeasuresQuery.Operator.GT; +import static org.sonar.server.measure.index.ProjectMeasuresQuery.Operator.LT; + +public class ProjectMeasuresIndexTextSearchTest { + + private static final String NCLOC = "ncloc"; + + private static final OrganizationDto ORG = OrganizationTesting.newOrganizationDto(); + + @Rule + public EsTester es = EsTester.create(); + @Rule + public ExpectedException expectedException = ExpectedException.none(); + @Rule + public UserSessionRule userSession = UserSessionRule.standalone(); + + private ProjectMeasuresIndexer projectMeasureIndexer = new ProjectMeasuresIndexer(null, es.client()); + private PermissionIndexerTester authorizationIndexer = new PermissionIndexerTester(es, projectMeasureIndexer); + private ProjectMeasuresIndex underTest = new ProjectMeasuresIndex(es.client(), new WebAuthorizationTypeSupport(userSession), System2.INSTANCE); + + @Test + public void match_exact_case_insensitive_name() { + index( + newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("Apache Struts")), + newDoc(newPrivateProjectDto(ORG).setUuid("sonarqube").setName("SonarQube"))); + + assertTextQueryResults("Apache Struts", "struts"); + assertTextQueryResults("APACHE STRUTS", "struts"); + assertTextQueryResults("APACHE struTS", "struts"); + } + + @Test + public void match_from_sub_name() { + index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("Apache Struts"))); + + assertTextQueryResults("truts", "struts"); + assertTextQueryResults("pache", "struts"); + assertTextQueryResults("apach", "struts"); + assertTextQueryResults("che stru", "struts"); + } + + @Test + public void match_name_with_dot() { + index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("Apache.Struts"))); + + assertTextQueryResults("apache struts", "struts"); + } + + @Test + public void match_partial_name() { + index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("XstrutsxXjavax"))); + + assertTextQueryResults("struts java", "struts"); + } + + @Test + public void match_partial_name_prefix_word1() { + index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("MyStruts.java"))); + + assertTextQueryResults("struts java", "struts"); + } + + @Test + public void match_partial_name_suffix_word1() { + index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("StrutsObject.java"))); + + assertTextQueryResults("struts java", "struts"); + } + + @Test + public void match_partial_name_prefix_word2() { + index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("MyStruts.xjava"))); + + assertTextQueryResults("struts java", "struts"); + } + + @Test + public void match_partial_name_suffix_word2() { + index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("MyStrutsObject.xjavax"))); + + assertTextQueryResults("struts java", "struts"); + } + + @Test + public void match_subset_of_document_terms() { + index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("Some.Struts.Project.java.old"))); + + assertTextQueryResults("struts java", "struts"); + } + + @Test + public void match_partial_match_prefix_and_suffix_everywhere() { + index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("MyStruts.javax"))); + + assertTextQueryResults("struts java", "struts"); + } + + @Test + public void ignore_empty_words() { + index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("Struts"))); + + assertTextQueryResults(" struts \n \n\n", "struts"); + } + + @Test + public void match_name_from_prefix() { + index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("Apache Struts"))); + + assertTextQueryResults("apach", "struts"); + assertTextQueryResults("ApA", "struts"); + assertTextQueryResults("AP", "struts"); + } + + @Test + public void match_name_from_two_words() { + index(newDoc(newPrivateProjectDto(ORG).setUuid("project").setName("ApacheStrutsFoundation"))); + + assertTextQueryResults("apache struts", "project"); + assertTextQueryResults("struts apache", "project"); + // Only one word is matching + assertNoResults("apache plugin"); + assertNoResults("project struts"); + } + + @Test + public void match_long_name() { + index( + newDoc(newPrivateProjectDto(ORG).setUuid("project1").setName("LongNameLongNameLongNameLongNameSonarQube")), + newDoc(newPrivateProjectDto(ORG).setUuid("project2").setName("LongNameLongNameLongNameLongNameSonarQubeX"))); + + assertTextQueryResults("LongNameLongNameLongNameLongNameSonarQube", "project1", "project2"); + } + + @Test + public void match_name_with_two_characters() { + index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("Apache Struts"))); + + assertTextQueryResults("st", "struts"); + assertTextQueryResults("tr", "struts"); + } + + @Test + public void match_exact_case_insensitive_key() { + index( + newDoc(newPrivateProjectDto(ORG).setUuid("project1").setName("Windows").setDbKey("project1")), + newDoc(newPrivateProjectDto(ORG).setUuid("project2").setName("apachee").setDbKey("project2"))); + + assertTextQueryResults("project1", "project1"); + assertTextQueryResults("PROJECT1", "project1"); + assertTextQueryResults("pRoJecT1", "project1"); + } + + @Test + public void match_key_with_dot() { + index( + newDoc(newPrivateProjectDto(ORG).setUuid("sonarqube").setName("SonarQube").setDbKey("org.sonarqube")), + newDoc(newPrivateProjectDto(ORG).setUuid("sq").setName("SQ").setDbKey("sonarqube"))); + + assertTextQueryResults("org.sonarqube", "sonarqube"); + assertNoResults("orgsonarqube"); + assertNoResults("org-sonarqube"); + assertNoResults("org:sonarqube"); + assertNoResults("org sonarqube"); + } + + @Test + public void match_key_with_dash() { + index( + newDoc(newPrivateProjectDto(ORG).setUuid("sonarqube").setName("SonarQube").setDbKey("org-sonarqube")), + newDoc(newPrivateProjectDto(ORG).setUuid("sq").setName("SQ").setDbKey("sonarqube"))); + + assertTextQueryResults("org-sonarqube", "sonarqube"); + assertNoResults("orgsonarqube"); + assertNoResults("org.sonarqube"); + assertNoResults("org:sonarqube"); + assertNoResults("org sonarqube"); + } + + @Test + public void match_key_with_colon() { + index( + newDoc(newPrivateProjectDto(ORG).setUuid("sonarqube").setName("SonarQube").setDbKey("org:sonarqube")), + newDoc(newPrivateProjectDto(ORG).setUuid("sq").setName("SQ").setDbKey("sonarqube"))); + + assertTextQueryResults("org:sonarqube", "sonarqube"); + assertNoResults("orgsonarqube"); + assertNoResults("org-sonarqube"); + assertNoResults("org_sonarqube"); + assertNoResults("org sonarqube"); + } + + @Test + public void match_key_having_all_special_characters() { + index(newDoc(newPrivateProjectDto(ORG).setUuid("sonarqube").setName("SonarQube").setDbKey("org.sonarqube:sonar-sérvèr_ç"))); + + assertTextQueryResults("org.sonarqube:sonar-sérvèr_ç", "sonarqube"); + } + + @Test + public void does_not_match_partial_key() { + index(newDoc(newPrivateProjectDto(ORG).setUuid("project").setName("some name").setDbKey("theKey"))); + + assertNoResults("theke"); + assertNoResults("hekey"); + } + + @Test + public void facets_take_into_account_text_search() { + index( + // docs with ncloc<1K + newDoc(newPrivateProjectDto(ORG).setName("Windows").setDbKey("project1"), NCLOC, 0d), + newDoc(newPrivateProjectDto(ORG).setName("apachee").setDbKey("project2"), NCLOC, 999d), + // docs with ncloc>=1K and ncloc<10K + newDoc(newPrivateProjectDto(ORG).setName("Apache").setDbKey("project3"), NCLOC, 1_000d), + // docs with ncloc>=100K and ncloc<500K + newDoc(newPrivateProjectDto(ORG).setName("Apache Foundation").setDbKey("project4"), NCLOC, 100_000d)); + + assertNclocFacet(new ProjectMeasuresQuery().setQueryText("apache"), 1L, 1L, 0L, 1L, 0L); + assertNclocFacet(new ProjectMeasuresQuery().setQueryText("PAch"), 1L, 1L, 0L, 1L, 0L); + assertNclocFacet(new ProjectMeasuresQuery().setQueryText("apache foundation"), 0L, 0L, 0L, 1L, 0L); + assertNclocFacet(new ProjectMeasuresQuery().setQueryText("project3"), 0L, 1L, 0L, 0L, 0L); + assertNclocFacet(new ProjectMeasuresQuery().setQueryText("project"), 0L, 0L, 0L, 0L, 0L); + } + + @Test + public void filter_by_metric_take_into_account_text_search() { + index( + newDoc(newPrivateProjectDto(ORG).setUuid("project1").setName("Windows").setDbKey("project1"), NCLOC, 30_000d), + newDoc(newPrivateProjectDto(ORG).setUuid("project2").setName("apachee").setDbKey("project2"), NCLOC, 40_000d), + newDoc(newPrivateProjectDto(ORG).setUuid("project3").setName("Apache").setDbKey("project3"), NCLOC, 50_000d), + newDoc(newPrivateProjectDto(ORG).setUuid("project4").setName("Apache").setDbKey("project4"), NCLOC, 60_000d)); + + assertResults(new ProjectMeasuresQuery().setQueryText("apache").addMetricCriterion(MetricCriterion.create(NCLOC, GT, 20_000d)), "project3", "project4", "project2"); + assertResults(new ProjectMeasuresQuery().setQueryText("apache").addMetricCriterion(MetricCriterion.create(NCLOC, LT, 55_000d)), "project3", "project2"); + assertResults(new ProjectMeasuresQuery().setQueryText("PAC").addMetricCriterion(MetricCriterion.create(NCLOC, LT, 55_000d)), "project3", "project2"); + assertResults(new ProjectMeasuresQuery().setQueryText("apachee").addMetricCriterion(MetricCriterion.create(NCLOC, GT, 30_000d)), "project2"); + assertResults(new ProjectMeasuresQuery().setQueryText("unknown").addMetricCriterion(MetricCriterion.create(NCLOC, GT, 20_000d))); + } + + private void index(ProjectMeasuresDoc... docs) { + es.putDocuments(TYPE_PROJECT_MEASURES, docs); + authorizationIndexer.allow(stream(docs).map(doc -> new IndexPermissions(doc.getId(), PROJECT).allowAnyone()).collect(toList())); + } + + private static ProjectMeasuresDoc newDoc(ComponentDto project) { + return new ProjectMeasuresDoc() + .setOrganizationUuid(project.getOrganizationUuid()) + .setId(project.uuid()) + .setKey(project.getDbKey()) + .setName(project.name()); + } + + private static ProjectMeasuresDoc newDoc(ComponentDto project, String metric1, Object value1) { + return newDoc(project).setMeasures(newArrayList(newMeasure(metric1, value1))); + } + + private static Map<String, Object> newMeasure(String key, Object value) { + return ImmutableMap.of("key", key, "value", value); + } + + private void assertResults(ProjectMeasuresQuery query, String... expectedProjectUuids) { + List<String> result = underTest.search(query, new SearchOptions()).getIds(); + assertThat(result).containsExactly(expectedProjectUuids); + } + + private void assertTextQueryResults(String queryText, String... expectedProjectUuids) { + assertResults(new ProjectMeasuresQuery().setQueryText(queryText), expectedProjectUuids); + } + + private void assertNoResults(String queryText) { + assertTextQueryResults(queryText); + } + + private void assertNclocFacet(ProjectMeasuresQuery query, Long... facetExpectedValues) { + checkArgument(facetExpectedValues.length == 5, "5 facet values is required"); + Facets facets = underTest.search(query, new SearchOptions().addFacets(NCLOC)).getFacets(); + assertThat(facets.get(NCLOC)).containsExactly( + entry("*-1000.0", facetExpectedValues[0]), + entry("1000.0-10000.0", facetExpectedValues[1]), + entry("10000.0-100000.0", facetExpectedValues[2]), + entry("100000.0-500000.0", facetExpectedValues[3]), + entry("500000.0-*", facetExpectedValues[4])); + } +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/measure/index/ProjectMeasuresQueryTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/measure/index/ProjectMeasuresQueryTest.java new file mode 100644 index 00000000000..4d1fca0083f --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/measure/index/ProjectMeasuresQueryTest.java @@ -0,0 +1,109 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.measure.index; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.measures.Metric.Level; +import org.sonar.server.measure.index.ProjectMeasuresQuery.MetricCriterion; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.groups.Tuple.tuple; +import static org.sonar.api.measures.Metric.Level.OK; +import static org.sonar.server.measure.index.ProjectMeasuresQuery.Operator.EQ; + +public class ProjectMeasuresQueryTest { + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + private ProjectMeasuresQuery underTest = new ProjectMeasuresQuery(); + + @Test + public void empty_query() { + assertThat(underTest.getMetricCriteria()).isEmpty(); + assertThat(underTest.getQualityGateStatus()).isEmpty(); + assertThat(underTest.getOrganizationUuid()).isEmpty(); + } + + @Test + public void add_metric_criterion() { + underTest.addMetricCriterion(MetricCriterion.create("coverage", EQ, 10d)); + + assertThat(underTest.getMetricCriteria()) + .extracting(MetricCriterion::getMetricKey, MetricCriterion::getOperator, MetricCriterion::getValue) + .containsOnly(tuple("coverage", EQ, 10d)); + } + + @Test + public void isNoData_returns_true_when_no_data() { + underTest.addMetricCriterion(MetricCriterion.createNoData("coverage")); + + assertThat(underTest.getMetricCriteria()) + .extracting(MetricCriterion::getMetricKey, MetricCriterion::isNoData) + .containsOnly(tuple("coverage", true)); + } + + @Test + public void isNoData_returns_false_when_data_exists() { + underTest.addMetricCriterion(MetricCriterion.create("coverage", EQ, 10d)); + + assertThat(underTest.getMetricCriteria()) + .extracting(MetricCriterion::getMetricKey, MetricCriterion::getOperator, MetricCriterion::isNoData) + .containsOnly(tuple("coverage", EQ, false)); + } + + @Test + public void set_quality_gate_status() { + underTest.setQualityGateStatus(OK); + + assertThat(underTest.getQualityGateStatus().get()).isEqualTo(Level.OK); + } + + @Test + public void default_sort_is_by_name() { + assertThat(underTest.getSort()).isEqualTo("name"); + } + + @Test + public void fail_to_set_null_sort() { + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("Sort cannot be null"); + + underTest.setSort(null); + } + + @Test + public void fail_to_get_value_when_no_data() { + expectedException.expect(IllegalStateException.class); + expectedException.expectMessage("The criterion for metric coverage has no data"); + + MetricCriterion.createNoData("coverage").getValue(); + } + + @Test + public void fail_to_get_operator_when_no_data() { + expectedException.expect(IllegalStateException.class); + expectedException.expectMessage("The criterion for metric coverage has no data"); + + MetricCriterion.createNoData("coverage").getOperator(); + } +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/measure/index/ProjectsEsModuleTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/measure/index/ProjectsEsModuleTest.java new file mode 100644 index 00000000000..3b460edc827 --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/measure/index/ProjectsEsModuleTest.java @@ -0,0 +1,35 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.measure.index; + +import org.junit.Test; +import org.sonar.core.platform.ComponentContainer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.core.platform.ComponentContainer.COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER; + +public class ProjectsEsModuleTest { + @Test + public void verify_count_of_added_components() { + ComponentContainer container = new ComponentContainer(); + new ProjectsEsModule().configure(container); + assertThat(container.size()).isEqualTo(3 + COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER); + } +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/notification/NotificationChannelTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/notification/NotificationChannelTest.java new file mode 100644 index 00000000000..9d90900df71 --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/notification/NotificationChannelTest.java @@ -0,0 +1,44 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.notification; + +import org.junit.Test; +import org.sonar.api.notifications.Notification; +import org.sonar.api.notifications.NotificationChannel; + +import static org.assertj.core.api.Assertions.assertThat; + +public class NotificationChannelTest { + + @Test + public void defaultMethods() { + NotificationChannel channel = new FakeNotificationChannel(); + assertThat(channel.getKey()).isEqualTo("FakeNotificationChannel"); + assertThat(channel.toString()).isEqualTo("FakeNotificationChannel"); + } + + private class FakeNotificationChannel extends NotificationChannel { + @Override + public boolean deliver(Notification notification, String username) { + return true; + } + } + +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/notification/NotificationDaemonTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/notification/NotificationDaemonTest.java new file mode 100644 index 00000000000..8a24e6b092c --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/notification/NotificationDaemonTest.java @@ -0,0 +1,105 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.notification; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InOrder; +import org.mockito.Mockito; +import org.mockito.verification.Timeout; +import org.sonar.api.config.PropertyDefinitions; +import org.sonar.api.config.internal.MapSettings; +import org.sonar.api.notifications.Notification; + +import static java.util.Collections.singleton; +import static org.mockito.ArgumentMatchers.anyCollection; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.when; + +public class NotificationDaemonTest { + private DefaultNotificationManager manager = mock(DefaultNotificationManager.class); + private NotificationService notificationService = mock(NotificationService.class); + private NotificationDaemon underTest; + private InOrder inOrder; + + @Before + public void setUp() throws Exception { + MapSettings settings = new MapSettings(new PropertyDefinitions(NotificationDaemon.class)).setProperty("sonar.notifications.delay", 1L); + + underTest = new NotificationDaemon(settings.asConfig(), manager, notificationService); + inOrder = Mockito.inOrder(notificationService); + } + + @After + public void tearDown() { + underTest.stop(); + } + + @Test + public void no_effect_when_no_notification() { + when(manager.getFromQueue()).thenReturn(null); + + underTest.start(); + inOrder.verify(notificationService, new Timeout(2000, Mockito.times(0))).deliverEmails(anyCollection()); + inOrder.verifyNoMoreInteractions(); + underTest.stop(); + } + + @Test + public void calls_both_api_and_deprecated_API() { + Notification notification = mock(Notification.class); + when(manager.getFromQueue()).thenReturn(notification).thenReturn(null); + + underTest.start(); + inOrder.verify(notificationService, timeout(2000)).deliverEmails(singleton(notification)); + inOrder.verify(notificationService).deliver(notification); + inOrder.verifyNoMoreInteractions(); + underTest.stop(); + } + + @Test + public void notifications_are_processed_one_by_one_even_with_new_API() { + Notification notification1 = mock(Notification.class); + Notification notification2 = mock(Notification.class); + Notification notification3 = mock(Notification.class); + Notification notification4 = mock(Notification.class); + when(manager.getFromQueue()) + .thenReturn(notification1) + .thenReturn(notification2) + .thenReturn(notification3) + .thenReturn(notification4) + .thenReturn(null); + + underTest.start(); + inOrder.verify(notificationService, timeout(2000)).deliverEmails(singleton(notification1)); + inOrder.verify(notificationService).deliver(notification1); + inOrder.verify(notificationService, timeout(2000)).deliverEmails(singleton(notification2)); + inOrder.verify(notificationService).deliver(notification2); + inOrder.verify(notificationService, timeout(2000)).deliverEmails(singleton(notification3)); + inOrder.verify(notificationService).deliver(notification3); + inOrder.verify(notificationService, timeout(2000)).deliverEmails(singleton(notification4)); + inOrder.verify(notificationService).deliver(notification4); + inOrder.verifyNoMoreInteractions(); + underTest.stop(); + + } +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/notification/NotificationMediumTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/notification/NotificationMediumTest.java new file mode 100644 index 00000000000..9476a84c983 --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/notification/NotificationMediumTest.java @@ -0,0 +1,242 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.notification; + +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.sonar.api.config.PropertyDefinitions; +import org.sonar.api.config.Settings; +import org.sonar.api.config.internal.MapSettings; +import org.sonar.api.notifications.Notification; +import org.sonar.api.notifications.NotificationChannel; +import org.sonar.db.DbClient; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class NotificationMediumTest { + private static String CREATOR_SIMON = "simon"; + private static String CREATOR_EVGENY = "evgeny"; + private static String ASSIGNEE_SIMON = "simon"; + + private DefaultNotificationManager manager = mock(DefaultNotificationManager.class); + private Notification notification = mock(Notification.class); + private NotificationChannel emailChannel = mock(NotificationChannel.class); + private NotificationChannel gtalkChannel = mock(NotificationChannel.class); + private NotificationDispatcher commentOnIssueAssignedToMe = mock(NotificationDispatcher.class); + private NotificationDispatcher commentOnIssueCreatedByMe = mock(NotificationDispatcher.class); + private NotificationDispatcher qualityGateChange = mock(NotificationDispatcher.class); + private DbClient dbClient = mock(DbClient.class); + private NotificationService service = new NotificationService(dbClient, new NotificationDispatcher[] {commentOnIssueAssignedToMe, commentOnIssueCreatedByMe, qualityGateChange}); + private NotificationDaemon underTest = null; + + private void setUpMocks() { + when(emailChannel.getKey()).thenReturn("email"); + when(gtalkChannel.getKey()).thenReturn("gtalk"); + when(commentOnIssueAssignedToMe.getKey()).thenReturn("CommentOnIssueAssignedToMe"); + when(commentOnIssueAssignedToMe.getType()).thenReturn("issue-changes"); + when(commentOnIssueCreatedByMe.getKey()).thenReturn("CommentOnIssueCreatedByMe"); + when(commentOnIssueCreatedByMe.getType()).thenReturn("issue-changes"); + when(qualityGateChange.getKey()).thenReturn("QGateChange"); + when(qualityGateChange.getType()).thenReturn("qgate-changes"); + when(manager.getFromQueue()).thenReturn(notification).thenReturn(null); + + MapSettings settings = new MapSettings(new PropertyDefinitions(NotificationDaemon.class)).setProperty("sonar.notifications.delay", 1L); + + underTest = new NotificationDaemon(settings.asConfig(), manager, service); + } + + /** + * Given: + * Simon wants to receive notifications by email on comments for reviews assigned to him or created by him. + * <p/> + * When: + * Freddy adds comment to review created by Simon and assigned to Simon. + * <p/> + * Then: + * Only one notification should be delivered to Simon by Email. + */ + @Test + public void scenario1() { + setUpMocks(); + doAnswer(addUser(ASSIGNEE_SIMON, emailChannel)).when(commentOnIssueAssignedToMe).dispatch(same(notification), any(NotificationDispatcher.Context.class)); + doAnswer(addUser(CREATOR_SIMON, emailChannel)).when(commentOnIssueCreatedByMe).dispatch(same(notification), any(NotificationDispatcher.Context.class)); + + underTest.start(); + verify(emailChannel, timeout(2000)).deliver(notification, ASSIGNEE_SIMON); + underTest.stop(); + + verify(gtalkChannel, never()).deliver(notification, ASSIGNEE_SIMON); + } + + /** + * Given: + * Evgeny wants to receive notification by GTalk on comments for reviews created by him. + * Simon wants to receive notification by Email on comments for reviews assigned to him. + * <p/> + * When: + * Freddy adds comment to review created by Evgeny and assigned to Simon. + * <p/> + * Then: + * Two notifications should be delivered - one to Simon by Email and another to Evgeny by GTalk. + */ + @Test + public void scenario2() { + setUpMocks(); + doAnswer(addUser(ASSIGNEE_SIMON, emailChannel)).when(commentOnIssueAssignedToMe).dispatch(same(notification), any(NotificationDispatcher.Context.class)); + doAnswer(addUser(CREATOR_EVGENY, gtalkChannel)).when(commentOnIssueCreatedByMe).dispatch(same(notification), any(NotificationDispatcher.Context.class)); + + underTest.start(); + verify(emailChannel, timeout(2000)).deliver(notification, ASSIGNEE_SIMON); + verify(gtalkChannel, timeout(2000)).deliver(notification, CREATOR_EVGENY); + underTest.stop(); + + verify(emailChannel, never()).deliver(notification, CREATOR_EVGENY); + verify(gtalkChannel, never()).deliver(notification, ASSIGNEE_SIMON); + } + + /** + * Given: + * Simon wants to receive notifications by Email and GTLak on comments for reviews assigned to him. + * <p/> + * When: + * Freddy adds comment to review created by Evgeny and assigned to Simon. + * <p/> + * Then: + * Two notifications should be delivered to Simon - one by Email and another by GTalk. + */ + @Test + public void scenario3() { + setUpMocks(); + doAnswer(addUser(ASSIGNEE_SIMON, new NotificationChannel[] {emailChannel, gtalkChannel})) + .when(commentOnIssueAssignedToMe).dispatch(same(notification), any(NotificationDispatcher.Context.class)); + + underTest.start(); + verify(emailChannel, timeout(2000)).deliver(notification, ASSIGNEE_SIMON); + verify(gtalkChannel, timeout(2000)).deliver(notification, ASSIGNEE_SIMON); + underTest.stop(); + + verify(emailChannel, never()).deliver(notification, CREATOR_EVGENY); + verify(gtalkChannel, never()).deliver(notification, CREATOR_EVGENY); + } + + /** + * Given: + * Nobody wants to receive notifications. + * <p/> + * When: + * Freddy adds comment to review created by Evgeny and assigned to Simon. + * <p/> + * Then: + * No notifications. + */ + @Test + public void scenario4() { + setUpMocks(); + + underTest.start(); + underTest.stop(); + + verify(emailChannel, never()).deliver(any(Notification.class), anyString()); + verify(gtalkChannel, never()).deliver(any(Notification.class), anyString()); + } + + // SONAR-4548 + @Test + public void shouldNotStopWhenException() { + setUpMocks(); + when(manager.getFromQueue()).thenThrow(new RuntimeException("Unexpected exception")).thenReturn(notification).thenReturn(null); + doAnswer(addUser(ASSIGNEE_SIMON, emailChannel)).when(commentOnIssueAssignedToMe).dispatch(same(notification), any(NotificationDispatcher.Context.class)); + doAnswer(addUser(CREATOR_SIMON, emailChannel)).when(commentOnIssueCreatedByMe).dispatch(same(notification), any(NotificationDispatcher.Context.class)); + + underTest.start(); + verify(emailChannel, timeout(2000)).deliver(notification, ASSIGNEE_SIMON); + underTest.stop(); + + verify(gtalkChannel, never()).deliver(notification, ASSIGNEE_SIMON); + } + + @Test + public void shouldNotAddNullAsUser() { + setUpMocks(); + doAnswer(addUser(null, gtalkChannel)).when(commentOnIssueCreatedByMe).dispatch(same(notification), any(NotificationDispatcher.Context.class)); + + underTest.start(); + underTest.stop(); + + verify(emailChannel, never()).deliver(any(Notification.class), anyString()); + verify(gtalkChannel, never()).deliver(any(Notification.class), anyString()); + } + + @Test + public void getDispatchers() { + setUpMocks(); + + assertThat(service.getDispatchers()).containsOnly(commentOnIssueAssignedToMe, commentOnIssueCreatedByMe, qualityGateChange); + } + + @Test + public void getDispatchers_empty() { + Settings settings = new MapSettings().setProperty("sonar.notifications.delay", 1L); + + service = new NotificationService(dbClient); + assertThat(service.getDispatchers()).hasSize(0); + } + + @Test + public void shouldLogEvery10Minutes() { + setUpMocks(); + // Emulate 2 notifications in DB + when(manager.getFromQueue()).thenReturn(notification).thenReturn(notification).thenReturn(null); + when(manager.count()).thenReturn(1L).thenReturn(0L); + underTest = spy(underTest); + // Emulate processing of each notification take 10 min to have a log each time + when(underTest.now()).thenReturn(0L).thenReturn(10 * 60 * 1000 + 1L).thenReturn(20 * 60 * 1000 + 2L); + underTest.start(); + verify(underTest, timeout(200)).log(0, 1, 10); + verify(underTest, timeout(200)).log(0, 0, 20); + underTest.stop(); + } + + private static Answer<Object> addUser(final String user, final NotificationChannel channel) { + return addUser(user, new NotificationChannel[] {channel}); + } + + private static Answer<Object> addUser(final String user, final NotificationChannel[] channels) { + return new Answer<Object>() { + public Object answer(InvocationOnMock invocation) { + for (NotificationChannel channel : channels) { + ((NotificationDispatcher.Context) invocation.getArguments()[1]).addUser(user, channel); + } + return null; + } + }; + } +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/notification/NotificationModuleTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/notification/NotificationModuleTest.java new file mode 100644 index 00000000000..b485602fcd3 --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/notification/NotificationModuleTest.java @@ -0,0 +1,35 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.notification; + +import org.junit.Test; +import org.sonar.core.platform.ComponentContainer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.core.platform.ComponentContainer.COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER; + +public class NotificationModuleTest { + @Test + public void verify_count_of_added_components() { + ComponentContainer container = new ComponentContainer(); + new NotificationModule().configure(container); + assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 5); + } +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/notification/NotificationTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/notification/NotificationTest.java new file mode 100644 index 00000000000..bc3400d44c6 --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/notification/NotificationTest.java @@ -0,0 +1,77 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.notification; + +import org.junit.Before; +import org.junit.Test; +import org.sonar.api.notifications.Notification; + +import static org.assertj.core.api.Assertions.assertThat; + +public class NotificationTest { + + private Notification notification; + + @Before + public void init() { + notification = new Notification("alerts").setDefaultMessage("There are new alerts").setFieldValue("alertCount", "42"); + } + + @Test + public void shouldReturnType() { + assertThat(notification.getType()).isEqualTo("alerts"); + } + + @Test + public void shouldReturnDefaultMessage() { + assertThat(notification.getDefaultMessage()).isEqualTo("There are new alerts"); + } + + @Test + public void shouldReturnToStringIfDefaultMessageNotSet() { + notification = new Notification("alerts").setFieldValue("alertCount", "42"); + System.out.println(notification); + assertThat(notification.getDefaultMessage()).contains("type='alerts'"); + assertThat(notification.getDefaultMessage()).contains("fields={alertCount=42}"); + } + + @Test + public void shouldReturnField() { + assertThat(notification.getFieldValue("alertCount")).isEqualTo("42"); + assertThat(notification.getFieldValue("fake")).isNull(); + + // default message is stored as field as well + assertThat(notification.getFieldValue("default_message")).isEqualTo("There are new alerts"); + } + + @Test + public void shouldEqual() { + assertThat(notification.equals("")).isFalse(); + assertThat(notification.equals(null)).isFalse(); + assertThat(notification.equals(notification)).isTrue(); + + Notification otherNotif = new Notification("alerts").setDefaultMessage("There are new alerts").setFieldValue("alertCount", "42"); + assertThat(otherNotif).isEqualTo(notification); + + otherNotif = new Notification("alerts").setDefaultMessage("There are new alerts").setFieldValue("alertCount", "15000"); + assertThat(otherNotif).isNotEqualTo(notification); + } + +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/notification/email/EmailNotificationChannelTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/notification/email/EmailNotificationChannelTest.java new file mode 100644 index 00000000000..7b57c9af448 --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/notification/email/EmailNotificationChannelTest.java @@ -0,0 +1,369 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.notification.email; + +import com.tngtech.java.junit.dataprovider.DataProvider; +import com.tngtech.java.junit.dataprovider.DataProviderRunner; +import com.tngtech.java.junit.dataprovider.UseDataProvider; +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import javax.mail.MessagingException; +import javax.mail.internet.MimeMessage; +import org.apache.commons.mail.EmailException; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.sonar.api.config.EmailSettings; +import org.sonar.api.notifications.Notification; +import org.sonar.server.issue.notification.EmailMessage; +import org.sonar.server.issue.notification.EmailTemplate; +import org.sonar.server.notification.email.EmailNotificationChannel.EmailDeliveryRequest; +import org.subethamail.wiser.Wiser; +import org.subethamail.wiser.WiserMessage; + +import static java.util.stream.Collectors.toMap; +import static java.util.stream.Collectors.toSet; +import static junit.framework.Assert.fail; +import static org.apache.commons.lang.RandomStringUtils.random; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +@RunWith(DataProviderRunner.class) +public class EmailNotificationChannelTest { + + private static final String SUBJECT_PREFIX = "[SONARQUBE]"; + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private Wiser smtpServer; + private EmailSettings configuration; + private EmailNotificationChannel underTest; + + @Before + public void setUp() { + smtpServer = new Wiser(0); + smtpServer.start(); + + configuration = mock(EmailSettings.class); + underTest = new EmailNotificationChannel(configuration, null, null); + } + + @After + public void tearDown() { + smtpServer.stop(); + } + + @Test + public void isActivated_returns_true_if_smpt_host_is_not_empty() { + when(configuration.getSmtpHost()).thenReturn(random(5)); + + assertThat(underTest.isActivated()).isTrue(); + } + + @Test + public void isActivated_returns_false_if_smpt_host_is_null() { + when(configuration.getSmtpHost()).thenReturn(null); + + assertThat(underTest.isActivated()).isFalse(); + } + + @Test + public void isActivated_returns_false_if_smpt_host_is_empty() { + when(configuration.getSmtpHost()).thenReturn(""); + + assertThat(underTest.isActivated()).isFalse(); + } + + @Test + public void shouldSendTestEmail() throws Exception { + configure(); + underTest.sendTestEmail("user@nowhere", "Test Message from SonarQube", "This is a test message from SonarQube."); + + List<WiserMessage> messages = smtpServer.getMessages(); + assertThat(messages).hasSize(1); + + MimeMessage email = messages.get(0).getMimeMessage(); + assertThat(email.getHeader("Content-Type", null)).isEqualTo("text/plain; charset=UTF-8"); + assertThat(email.getHeader("From", ",")).isEqualTo("SonarQube from NoWhere <server@nowhere>"); + assertThat(email.getHeader("To", null)).isEqualTo("<user@nowhere>"); + assertThat(email.getHeader("Subject", null)).isEqualTo("[SONARQUBE] Test Message from SonarQube"); + assertThat((String) email.getContent()).startsWith("This is a test message from SonarQube."); + } + + @Test + public void shouldThrowAnExceptionWhenUnableToSendTestEmail() { + configure(); + smtpServer.stop(); + + try { + underTest.sendTestEmail("user@nowhere", "Test Message from SonarQube", "This is a test message from SonarQube."); + fail(); + } catch (EmailException e) { + // expected + } + } + + @Test + public void shouldNotSendEmailWhenHostnameNotConfigured() { + EmailMessage emailMessage = new EmailMessage() + .setTo("user@nowhere") + .setSubject("Foo") + .setPlainTextMessage("Bar"); + boolean delivered = underTest.deliver(emailMessage); + assertThat(smtpServer.getMessages()).isEmpty(); + assertThat(delivered).isFalse(); + } + + @Test + public void shouldSendThreadedEmail() throws Exception { + configure(); + EmailMessage emailMessage = new EmailMessage() + .setMessageId("reviews/view/1") + .setFrom("Full Username") + .setTo("user@nowhere") + .setSubject("Review #3") + .setPlainTextMessage("I'll take care of this violation."); + boolean delivered = underTest.deliver(emailMessage); + + List<WiserMessage> messages = smtpServer.getMessages(); + assertThat(messages).hasSize(1); + + MimeMessage email = messages.get(0).getMimeMessage(); + + assertThat(email.getHeader("Content-Type", null)).isEqualTo("text/plain; charset=UTF-8"); + + assertThat(email.getHeader("In-Reply-To", null)).isEqualTo("<reviews/view/1@nemo.sonarsource.org>"); + assertThat(email.getHeader("References", null)).isEqualTo("<reviews/view/1@nemo.sonarsource.org>"); + + assertThat(email.getHeader("List-ID", null)).isEqualTo("SonarQube <sonar.nemo.sonarsource.org>"); + assertThat(email.getHeader("List-Archive", null)).isEqualTo("http://nemo.sonarsource.org"); + + assertThat(email.getHeader("From", ",")).isEqualTo("\"Full Username (SonarQube from NoWhere)\" <server@nowhere>"); + assertThat(email.getHeader("To", null)).isEqualTo("<user@nowhere>"); + assertThat(email.getHeader("Subject", null)).isEqualTo("[SONARQUBE] Review #3"); + assertThat((String) email.getContent()).startsWith("I'll take care of this violation."); + assertThat(delivered).isTrue(); + } + + @Test + public void shouldSendNonThreadedEmail() throws Exception { + configure(); + EmailMessage emailMessage = new EmailMessage() + .setTo("user@nowhere") + .setSubject("Foo") + .setPlainTextMessage("Bar"); + boolean delivered = underTest.deliver(emailMessage); + + List<WiserMessage> messages = smtpServer.getMessages(); + assertThat(messages).hasSize(1); + + MimeMessage email = messages.get(0).getMimeMessage(); + + assertThat(email.getHeader("Content-Type", null)).isEqualTo("text/plain; charset=UTF-8"); + + assertThat(email.getHeader("In-Reply-To", null)).isNull(); + assertThat(email.getHeader("References", null)).isNull(); + + assertThat(email.getHeader("List-ID", null)).isEqualTo("SonarQube <sonar.nemo.sonarsource.org>"); + assertThat(email.getHeader("List-Archive", null)).isEqualTo("http://nemo.sonarsource.org"); + + assertThat(email.getHeader("From", null)).isEqualTo("SonarQube from NoWhere <server@nowhere>"); + assertThat(email.getHeader("To", null)).isEqualTo("<user@nowhere>"); + assertThat(email.getHeader("Subject", null)).isEqualTo("[SONARQUBE] Foo"); + assertThat((String) email.getContent()).startsWith("Bar"); + assertThat(delivered).isTrue(); + } + + @Test + public void shouldNotThrowAnExceptionWhenUnableToSendEmail() { + configure(); + smtpServer.stop(); + + EmailMessage emailMessage = new EmailMessage() + .setTo("user@nowhere") + .setSubject("Foo") + .setPlainTextMessage("Bar"); + boolean delivered = underTest.deliver(emailMessage); + + assertThat(delivered).isFalse(); + } + + @Test + public void shouldSendTestEmailWithSTARTTLS() { + smtpServer.getServer().setEnableTLS(true); + smtpServer.getServer().setRequireTLS(true); + configure(); + when(configuration.getSecureConnection()).thenReturn("STARTTLS"); + + try { + underTest.sendTestEmail("user@nowhere", "Test Message from SonarQube", "This is a test message from SonarQube."); + fail("An SSL exception was expected a a proof that STARTTLS is enabled"); + } catch (EmailException e) { + // We don't have a SSL certificate so we are expecting a SSL error + assertThat(e.getCause().getMessage()).isEqualTo("Could not convert socket to TLS"); + } + } + + @Test + public void deliverAll_has_no_effect_if_set_is_empty() { + EmailSettings emailSettings = mock(EmailSettings.class); + EmailNotificationChannel underTest = new EmailNotificationChannel(emailSettings, null, null); + + int count = underTest.deliverAll(Collections.emptySet()); + + assertThat(count).isZero(); + verifyZeroInteractions(emailSettings); + assertThat(smtpServer.getMessages()).isEmpty(); + } + + @Test + public void deliverAll_has_no_effect_if_smtp_host_is_null() { + EmailSettings emailSettings = mock(EmailSettings.class); + when(emailSettings.getSmtpHost()).thenReturn(null); + Set<EmailDeliveryRequest> requests = IntStream.range(0, 1 + new Random().nextInt(10)) + .mapToObj(i -> new EmailDeliveryRequest("foo" + i + "@moo", mock(Notification.class))) + .collect(toSet()); + EmailNotificationChannel underTest = new EmailNotificationChannel(emailSettings, null, null); + + int count = underTest.deliverAll(requests); + + assertThat(count).isZero(); + verify(emailSettings).getSmtpHost(); + verifyNoMoreInteractions(emailSettings); + assertThat(smtpServer.getMessages()).isEmpty(); + } + + @Test + @UseDataProvider("emptyStrings") + public void deliverAll_ignores_requests_which_recipient_is_empty(String emptyString) { + EmailSettings emailSettings = mock(EmailSettings.class); + when(emailSettings.getSmtpHost()).thenReturn(null); + Set<EmailDeliveryRequest> requests = IntStream.range(0, 1 + new Random().nextInt(10)) + .mapToObj(i -> new EmailDeliveryRequest(emptyString, mock(Notification.class))) + .collect(toSet()); + EmailNotificationChannel underTest = new EmailNotificationChannel(emailSettings, null, null); + + int count = underTest.deliverAll(requests); + + assertThat(count).isZero(); + verify(emailSettings).getSmtpHost(); + verifyNoMoreInteractions(emailSettings); + assertThat(smtpServer.getMessages()).isEmpty(); + } + + @Test + public void deliverAll_returns_count_of_request_for_which_at_least_one_formatter_accept_it() throws MessagingException, IOException { + String recipientEmail = "foo@donut"; + configure(); + Notification notification1 = mock(Notification.class); + Notification notification2 = mock(Notification.class); + Notification notification3 = mock(Notification.class); + EmailTemplate template1 = mock(EmailTemplate.class); + EmailTemplate template3 = mock(EmailTemplate.class); + EmailMessage emailMessage1 = new EmailMessage().setTo(recipientEmail).setSubject("sub11").setPlainTextMessage("msg11"); + EmailMessage emailMessage3 = new EmailMessage().setTo(recipientEmail).setSubject("sub3").setPlainTextMessage("msg3"); + when(template1.format(notification1)).thenReturn(emailMessage1); + when(template3.format(notification3)).thenReturn(emailMessage3); + Set<EmailDeliveryRequest> requests = Stream.of(notification1, notification2, notification3) + .map(t -> new EmailDeliveryRequest(recipientEmail, t)) + .collect(toSet()); + EmailNotificationChannel underTest = new EmailNotificationChannel(configuration, new EmailTemplate[] {template1, template3}, null); + + int count = underTest.deliverAll(requests); + + assertThat(count).isEqualTo(2); + assertThat(smtpServer.getMessages()).hasSize(2); + Map<String, MimeMessage> messagesBySubject = smtpServer.getMessages().stream() + .map(t -> { + try { + return t.getMimeMessage(); + } catch (MessagingException e) { + throw new RuntimeException(e); + } + }) + .collect(toMap(t -> { + try { + return t.getSubject(); + } catch (MessagingException e) { + throw new RuntimeException(e); + } + }, t -> t)); + + assertThat((String) messagesBySubject.get(SUBJECT_PREFIX + " " + emailMessage1.getSubject()).getContent()) + .contains(emailMessage1.getMessage()); + assertThat((String) messagesBySubject.get(SUBJECT_PREFIX + " " + emailMessage3.getSubject()).getContent()) + .contains(emailMessage3.getMessage()); + } + + @Test + public void deliverAll_ignores_multiple_templates_by_notification_and_takes_the_first_one_only() throws MessagingException, IOException { + String recipientEmail = "foo@donut"; + configure(); + Notification notification1 = mock(Notification.class); + EmailTemplate template11 = mock(EmailTemplate.class); + EmailTemplate template12 = mock(EmailTemplate.class); + EmailMessage emailMessage11 = new EmailMessage().setTo(recipientEmail).setSubject("sub11").setPlainTextMessage("msg11"); + EmailMessage emailMessage12 = new EmailMessage().setTo(recipientEmail).setSubject("sub12").setPlainTextMessage("msg12"); + when(template11.format(notification1)).thenReturn(emailMessage11); + when(template12.format(notification1)).thenReturn(emailMessage12); + EmailDeliveryRequest request = new EmailDeliveryRequest(recipientEmail, notification1); + EmailNotificationChannel underTest = new EmailNotificationChannel(configuration, new EmailTemplate[] {template11, template12}, null); + + int count = underTest.deliverAll(Collections.singleton(request)); + + assertThat(count).isEqualTo(1); + assertThat(smtpServer.getMessages()).hasSize(1); + assertThat((String) smtpServer.getMessages().iterator().next().getMimeMessage().getContent()) + .contains(emailMessage11.getMessage()); + } + + @DataProvider + public static Object[][] emptyStrings() { + return new Object[][] { + {""}, + {" "}, + {" \n "} + }; + } + + private void configure() { + when(configuration.getSmtpHost()).thenReturn("localhost"); + when(configuration.getSmtpPort()).thenReturn(smtpServer.getServer().getPort()); + when(configuration.getFrom()).thenReturn("server@nowhere"); + when(configuration.getFromName()).thenReturn("SonarQube from NoWhere"); + when(configuration.getPrefix()).thenReturn(SUBJECT_PREFIX); + when(configuration.getServerBaseURL()).thenReturn("http://nemo.sonarsource.org"); + } + +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/BackendCleanupTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/BackendCleanupTest.java new file mode 100644 index 00000000000..30c65d1fee0 --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/BackendCleanupTest.java @@ -0,0 +1,157 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform; + +import java.util.Random; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.api.utils.System2; +import org.sonar.db.DbTester; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.component.ComponentTesting; +import org.sonar.db.organization.OrganizationDto; +import org.sonar.db.organization.OrganizationTesting; +import org.sonar.db.property.PropertyDto; +import org.sonar.db.rule.RuleTesting; +import org.sonar.server.component.index.ComponentDoc; +import org.sonar.server.component.index.ComponentIndexDefinition; +import org.sonar.server.component.index.ComponentIndexer; +import org.sonar.server.es.EsTester; +import org.sonar.server.issue.IssueDocTesting; +import org.sonar.server.issue.index.IssueIndexDefinition; +import org.sonar.server.measure.index.ProjectMeasuresDoc; +import org.sonar.server.measure.index.ProjectMeasuresIndexDefinition; +import org.sonar.server.rule.index.RuleDoc; +import org.sonar.server.rule.index.RuleIndexDefinition; +import org.sonar.server.view.index.ViewDoc; +import org.sonar.server.view.index.ViewIndexDefinition; + +import static com.google.common.collect.Lists.newArrayList; +import static org.assertj.core.api.Assertions.assertThat; + +public class BackendCleanupTest { + + @Rule + public EsTester es = EsTester.create(); + + @Rule + public DbTester dbTester = DbTester.create(System2.INSTANCE); + + private BackendCleanup underTest = new BackendCleanup(es.client(), dbTester.getDbClient()); + private OrganizationDto organization; + + @Before + public void setUp() { + organization = OrganizationTesting.newOrganizationDto(); + } + + @Test + public void clear_db() { + insertSomeData(); + + underTest.clearDb(); + + assertThat(dbTester.countRowsOfTable("projects")).isEqualTo(0); + assertThat(dbTester.countRowsOfTable("snapshots")).isEqualTo(0); + assertThat(dbTester.countRowsOfTable("rules")).isEqualTo(0); + assertThat(dbTester.countRowsOfTable("properties")).isEqualTo(0); + } + + @Test + public void clear_indexes() { + es.putDocuments(IssueIndexDefinition.TYPE_ISSUE, IssueDocTesting.newDoc()); + es.putDocuments(RuleIndexDefinition.TYPE_RULE, newRuleDoc()); + es.putDocuments(ComponentIndexDefinition.TYPE_COMPONENT, newComponentDoc()); + + underTest.clearIndexes(); + + assertThat(es.countDocuments(IssueIndexDefinition.TYPE_ISSUE)).isEqualTo(0); + assertThat(es.countDocuments(ComponentIndexDefinition.TYPE_COMPONENT)).isEqualTo(0); + } + + @Test + public void clear_all() { + insertSomeData(); + + es.putDocuments(IssueIndexDefinition.TYPE_ISSUE, IssueDocTesting.newDoc()); + es.putDocuments(RuleIndexDefinition.TYPE_RULE, newRuleDoc()); + es.putDocuments(ComponentIndexDefinition.TYPE_COMPONENT, newComponentDoc()); + + underTest.clearAll(); + + assertThat(es.countDocuments(IssueIndexDefinition.TYPE_ISSUE)).isEqualTo(0); + assertThat(es.countDocuments(RuleIndexDefinition.TYPE_RULE)).isEqualTo(0); + assertThat(es.countDocuments(ComponentIndexDefinition.TYPE_COMPONENT)).isEqualTo(0); + + assertThat(dbTester.countRowsOfTable("projects")).isEqualTo(0); + assertThat(dbTester.countRowsOfTable("snapshots")).isEqualTo(0); + assertThat(dbTester.countRowsOfTable("rules")).isEqualTo(0); + assertThat(dbTester.countRowsOfTable("properties")).isEqualTo(0); + } + + @Test + public void reset_data() { + insertSomeData(); + + es.putDocuments(IssueIndexDefinition.TYPE_ISSUE, IssueDocTesting.newDoc()); + es.putDocuments(ViewIndexDefinition.TYPE_VIEW, new ViewDoc().setUuid("CDEF").setProjects(newArrayList("DEFG"))); + es.putDocuments(RuleIndexDefinition.TYPE_RULE, newRuleDoc()); + es.putDocuments(ProjectMeasuresIndexDefinition.TYPE_PROJECT_MEASURES, new ProjectMeasuresDoc() + .setId("PROJECT") + .setKey("Key") + .setName("Name")); + es.putDocuments(ComponentIndexDefinition.TYPE_COMPONENT, newComponentDoc()); + + underTest.resetData(); + + assertThat(dbTester.countRowsOfTable("projects")).isZero(); + assertThat(dbTester.countRowsOfTable("snapshots")).isZero(); + assertThat(dbTester.countRowsOfTable("properties")).isZero(); + assertThat(es.countDocuments(IssueIndexDefinition.TYPE_ISSUE)).isZero(); + assertThat(es.countDocuments(ViewIndexDefinition.TYPE_VIEW)).isZero(); + assertThat(es.countDocuments(ProjectMeasuresIndexDefinition.TYPE_PROJECT_MEASURES)).isZero(); + assertThat(es.countDocuments(ComponentIndexDefinition.TYPE_COMPONENT)).isZero(); + + // Rules should not be removed + assertThat(dbTester.countRowsOfTable("rules")).isEqualTo(1); + assertThat(es.countDocuments(RuleIndexDefinition.TYPE_RULE)).isEqualTo(1); + } + + private void insertSomeData() { + OrganizationDto organization = dbTester.organizations().insert(); + ComponentDto project = dbTester.components().insertPrivateProject(organization); + dbTester.components().insertSnapshot(project); + dbTester.rules().insert(); + dbTester.properties().insertProperty(new PropertyDto() + .setKey("sonar.profile.java") + .setValue("Sonar Way") + .setResourceId(project.getId()) + ); + } + + private static RuleDoc newRuleDoc() { + return new RuleDoc().setId(new Random().nextInt(942)).setKey(RuleTesting.XOO_X1.toString()).setRepository(RuleTesting.XOO_X1.repository()); + } + + private ComponentDoc newComponentDoc() { + return ComponentIndexer.toDocument(ComponentTesting.newPrivateProjectDto(organization)); + } +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/ClassLoaderUtilsTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/ClassLoaderUtilsTest.java new file mode 100644 index 00000000000..f81955071ec --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/ClassLoaderUtilsTest.java @@ -0,0 +1,76 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform; + +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Collection; +import org.junit.Before; +import org.junit.Test; +import org.sonar.server.util.ClassLoaderUtils; + +import static org.apache.commons.lang.StringUtils.endsWith; +import static org.assertj.core.api.Assertions.assertThat; + +public class ClassLoaderUtilsTest { + + private ClassLoader classLoader; + + @Before + public void prepareClassLoader() { + // This JAR file has the three following files : + // org/sonar/sqale/app/copyright.txt + // org/sonar/sqale/app/README.md + // org/sonar/other/other.txt + URL jarUrl = getClass().getResource("/org/sonar/server/platform/ClassLoaderUtilsTest/ClassLoaderUtilsTest.jar"); + classLoader = new URLClassLoader(new URL[] {jarUrl}, /* no parent classloader */null); + } + + @Test + public void listResources_unknown_root() { + Collection<String> strings = ClassLoaderUtils.listResources(classLoader, "unknown/directory", s -> true); + assertThat(strings).isEmpty(); + } + + @Test + public void listResources_all() { + Collection<String> strings = ClassLoaderUtils.listResources(classLoader, "org/sonar/sqale", s -> true); + assertThat(strings).containsOnly( + "org/sonar/sqale/", + "org/sonar/sqale/app/", + "org/sonar/sqale/app/copyright.txt", + "org/sonar/sqale/app/README.md"); + } + + @Test + public void listResources_use_predicate() { + Collection<String> strings = ClassLoaderUtils.listResources(classLoader, "org/sonar/sqale", s -> endsWith(s, "md")); + assertThat(strings).containsOnly("org/sonar/sqale/app/README.md"); + } + + @Test + public void listFiles() { + Collection<String> strings = ClassLoaderUtils.listFiles(classLoader, "org/sonar/sqale"); + assertThat(strings).containsOnly( + "org/sonar/sqale/app/copyright.txt", + "org/sonar/sqale/app/README.md"); + } + +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/ClusterVerificationTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/ClusterVerificationTest.java new file mode 100644 index 00000000000..590bca2cb09 --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/ClusterVerificationTest.java @@ -0,0 +1,85 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.utils.MessageException; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ClusterVerificationTest { + + private static final String ERROR_MESSAGE = "Cluster mode can't be enabled. Please install the Data Center Edition. More details at https://redirect.sonarsource.com/editions/datacenter.html."; + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + private WebServer webServer = mock(WebServer.class); + private ClusterFeature feature = mock(ClusterFeature.class); + + @Test + public void throw_MessageException_if_cluster_is_enabled_but_HA_plugin_is_not_installed() { + when(webServer.isStandalone()).thenReturn(false); + + ClusterVerification underTest = new ClusterVerification(webServer); + + expectedException.expect(MessageException.class); + expectedException.expectMessage(ERROR_MESSAGE); + underTest.start(); + } + + @Test + public void throw_MessageException_if_cluster_is_enabled_but_HA_feature_is_not_enabled() { + when(webServer.isStandalone()).thenReturn(false); + when(feature.isEnabled()).thenReturn(false); + ClusterVerification underTest = new ClusterVerification(webServer, feature); + + expectedException.expect(MessageException.class); + expectedException.expectMessage(ERROR_MESSAGE); + underTest.start(); + } + + @Test + public void do_not_fail_if_cluster_is_enabled_and_HA_feature_is_enabled() { + when(webServer.isStandalone()).thenReturn(false); + when(feature.isEnabled()).thenReturn(true); + ClusterVerification underTest = new ClusterVerification(webServer, feature); + + // no failure + underTest.start(); + underTest.stop(); + } + + @Test + public void do_not_fail_if_cluster_is_disabled() { + when(webServer.isStandalone()).thenReturn(true); + + ClusterVerification underTest = new ClusterVerification(webServer); + + // no failure + underTest.start(); + underTest.stop(); + } + + +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/DatabaseServerCompatibilityTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/DatabaseServerCompatibilityTest.java new file mode 100644 index 00000000000..3de7281760b --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/DatabaseServerCompatibilityTest.java @@ -0,0 +1,104 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform; + +import java.util.Optional; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.config.internal.MapSettings; +import org.sonar.api.utils.MessageException; +import org.sonar.api.utils.log.LogTester; +import org.sonar.api.utils.log.LoggerLevel; +import org.sonar.server.platform.db.migration.version.DatabaseVersion; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class DatabaseServerCompatibilityTest { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + @Rule + public LogTester logTester = new LogTester(); + @Rule + public ExpectedException expectedException = ExpectedException.none(); + private MapSettings settings = new MapSettings(); + + @Test + public void fail_if_requires_downgrade() { + thrown.expect(MessageException.class); + thrown.expectMessage("Database was upgraded to a more recent version of SonarQube. " + + "A backup must probably be restored or the DB settings are incorrect."); + + DatabaseVersion version = mock(DatabaseVersion.class); + when(version.getStatus()).thenReturn(DatabaseVersion.Status.REQUIRES_DOWNGRADE); + new DatabaseServerCompatibility(version, settings.asConfig()).start(); + } + + @Test + public void fail_if_requires_firstly_to_upgrade_to_lts() { + thrown.expect(MessageException.class); + thrown.expectMessage("Current version is too old. Please upgrade to Long Term Support version firstly."); + + DatabaseVersion version = mock(DatabaseVersion.class); + when(version.getStatus()).thenReturn(DatabaseVersion.Status.REQUIRES_UPGRADE); + when(version.getVersion()).thenReturn(Optional.of(12L)); + new DatabaseServerCompatibility(version, settings.asConfig()).start(); + } + + @Test + public void log_warning_if_requires_upgrade() { + DatabaseVersion version = mock(DatabaseVersion.class); + when(version.getStatus()).thenReturn(DatabaseVersion.Status.REQUIRES_UPGRADE); + when(version.getVersion()).thenReturn(Optional.of(DatabaseVersion.MIN_UPGRADE_VERSION)); + new DatabaseServerCompatibility(version, settings.asConfig()).start(); + + assertThat(logTester.logs()).hasSize(2); + assertThat(logTester.logs(LoggerLevel.WARN)).contains( + "The database must be manually upgraded. Please backup the database and browse /setup. " + + "For more information: https://docs.sonarqube.org/latest/setup/upgrading", + "\n################################################################################\n" + + " The database must be manually upgraded. Please backup the database and browse /setup. " + + "For more information: https://docs.sonarqube.org/latest/setup/upgrading\n" + + "################################################################################"); + } + + @Test + public void do_nothing_if_up_to_date() { + DatabaseVersion version = mock(DatabaseVersion.class); + when(version.getStatus()).thenReturn(DatabaseVersion.Status.UP_TO_DATE); + new DatabaseServerCompatibility(version, settings.asConfig()).start(); + // no error + } + + @Test + public void upgrade_automatically_if_blue_green_deployment() { + settings.setProperty("sonar.blueGreenEnabled", "true"); + DatabaseVersion version = mock(DatabaseVersion.class); + when(version.getStatus()).thenReturn(DatabaseVersion.Status.REQUIRES_UPGRADE); + when(version.getVersion()).thenReturn(Optional.of(DatabaseVersion.MIN_UPGRADE_VERSION)); + + new DatabaseServerCompatibility(version, settings.asConfig()).start(); + + assertThat(logTester.logs(LoggerLevel.WARN)).isEmpty(); + } +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/DefaultServerUpgradeStatusTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/DefaultServerUpgradeStatusTest.java new file mode 100644 index 00000000000..050fc56cbac --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/DefaultServerUpgradeStatusTest.java @@ -0,0 +1,91 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform; + +import java.util.Optional; +import org.junit.Before; +import org.junit.Test; +import org.sonar.api.config.internal.ConfigurationBridge; +import org.sonar.api.config.internal.MapSettings; +import org.sonar.server.platform.db.migration.step.MigrationSteps; +import org.sonar.server.platform.db.migration.version.DatabaseVersion; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class DefaultServerUpgradeStatusTest { + private static final long LAST_VERSION = 150; + private MigrationSteps migrationSteps = mock(MigrationSteps.class); + private DatabaseVersion dbVersion = mock(DatabaseVersion.class); + private MapSettings settings = new MapSettings(); + private DefaultServerUpgradeStatus underTest = new DefaultServerUpgradeStatus(dbVersion, migrationSteps, new ConfigurationBridge(settings)); + + @Before + public void setUp() throws Exception { + when(migrationSteps.getMaxMigrationNumber()).thenReturn(LAST_VERSION); + } + + @Test + public void shouldBeFreshInstallation() { + when(migrationSteps.getMaxMigrationNumber()).thenReturn(150L); + when(dbVersion.getVersion()).thenReturn(Optional.empty()); + + underTest.start(); + + assertThat(underTest.isFreshInstall()).isTrue(); + assertThat(underTest.isUpgraded()).isFalse(); + assertThat(underTest.getInitialDbVersion()).isEqualTo(-1); + } + + @Test + public void shouldBeUpgraded() { + when(dbVersion.getVersion()).thenReturn(Optional.of(50L)); + + underTest.start(); + + assertThat(underTest.isFreshInstall()).isFalse(); + assertThat(underTest.isUpgraded()).isTrue(); + assertThat(underTest.getInitialDbVersion()).isEqualTo(50); + } + + @Test + public void shouldNotBeUpgraded() { + when(dbVersion.getVersion()).thenReturn(Optional.of(LAST_VERSION)); + + underTest.start(); + + assertThat(underTest.isFreshInstall()).isFalse(); + assertThat(underTest.isUpgraded()).isFalse(); + assertThat(underTest.getInitialDbVersion()).isEqualTo((int) LAST_VERSION); + } + + @Test + public void isBlueGreen() { + settings.clear(); + assertThat(underTest.isBlueGreen()).isFalse(); + + settings.setProperty("sonar.blueGreenEnabled", true); + assertThat(underTest.isBlueGreen()).isTrue(); + + settings.setProperty("sonar.blueGreenEnabled", false); + assertThat(underTest.isBlueGreen()).isFalse(); + } +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/PersistentSettingsTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/PersistentSettingsTest.java new file mode 100644 index 00000000000..884c7d343de --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/PersistentSettingsTest.java @@ -0,0 +1,66 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform; + +import org.junit.Rule; +import org.junit.Test; +import org.sonar.api.config.Settings; +import org.sonar.api.config.internal.MapSettings; +import org.sonar.api.utils.System2; +import org.sonar.db.DbTester; +import org.sonar.server.setting.SettingsChangeNotifier; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +public class PersistentSettingsTest { + @Rule + public DbTester dbTester = DbTester.create(System2.INSTANCE); + private Settings delegate = new MapSettings(); + private SettingsChangeNotifier changeNotifier = mock(SettingsChangeNotifier.class); + private PersistentSettings underTest = new PersistentSettings(delegate, dbTester.getDbClient(), changeNotifier); + + @Test + public void insert_property_into_database_and_notify_extensions() { + assertThat(underTest.getString("foo")).isNull(); + + underTest.saveProperty("foo", "bar"); + + assertThat(underTest.getString("foo")).isEqualTo("bar"); + assertThat(dbTester.getDbClient().propertiesDao().selectGlobalProperty("foo").getValue()).isEqualTo("bar"); + verify(changeNotifier).onGlobalPropertyChange("foo", "bar"); + } + + @Test + public void delete_property_from_database_and_notify_extensions() { + underTest.saveProperty("foo", "bar"); + underTest.saveProperty("foo", null); + + assertThat(underTest.getString("foo")).isNull(); + assertThat(dbTester.getDbClient().propertiesDao().selectGlobalProperty("foo")).isNull(); + verify(changeNotifier).onGlobalPropertyChange("foo", null); + } + + @Test + public void getSettings_returns_delegate() { + assertThat(underTest.getSettings()).isSameAs(delegate); + } +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/StartupMetadataPersisterTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/StartupMetadataPersisterTest.java new file mode 100644 index 00000000000..c4c84ee1b94 --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/StartupMetadataPersisterTest.java @@ -0,0 +1,56 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.CoreProperties; +import org.sonar.api.utils.DateUtils; +import org.sonar.api.utils.System2; +import org.sonar.db.DbTester; +import org.sonar.db.property.PropertyDto; + +import static org.assertj.core.api.Assertions.assertThat; + +public class StartupMetadataPersisterTest { + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + @Rule + public DbTester dbTester = DbTester.create(System2.INSTANCE); + + private StartupMetadata metadata = new StartupMetadata(123_456_789L); + private StartupMetadataPersister underTest = new StartupMetadataPersister(metadata, dbTester.getDbClient()); + + @Test + public void persist_metadata_at_startup() { + underTest.start(); + + assertPersistedProperty(CoreProperties.SERVER_STARTTIME, DateUtils.formatDateTime(metadata.getStartedAt())); + + underTest.stop(); + } + + private void assertPersistedProperty(String propertyKey, String expectedValue) { + PropertyDto prop = dbTester.getDbClient().propertiesDao().selectGlobalProperty(dbTester.getSession(), propertyKey); + assertThat(prop.getValue()).isEqualTo(expectedValue); + } +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/WebCoreExtensionsInstallerTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/WebCoreExtensionsInstallerTest.java new file mode 100644 index 00000000000..b89d6c2739b --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/WebCoreExtensionsInstallerTest.java @@ -0,0 +1,111 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.stream.Stream; +import org.junit.Test; +import org.sonar.api.SonarRuntime; +import org.sonar.api.batch.ScannerSide; +import org.sonar.api.ce.ComputeEngineSide; +import org.sonar.api.server.ServerSide; +import org.sonar.core.extension.CoreExtension; +import org.sonar.core.extension.CoreExtensionRepository; +import org.sonar.core.platform.ComponentContainer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.sonar.core.extension.CoreExtensionsInstaller.noAdditionalSideFilter; +import static org.sonar.core.extension.CoreExtensionsInstaller.noExtensionFilter; + +public class WebCoreExtensionsInstallerTest { + private SonarRuntime sonarRuntime = mock(SonarRuntime.class); + private CoreExtensionRepository coreExtensionRepository = mock(CoreExtensionRepository.class); + + private WebCoreExtensionsInstaller underTest = new WebCoreExtensionsInstaller(sonarRuntime, coreExtensionRepository); + + @Test + public void install_only_adds_ServerSide_annotated_extension_to_container() { + when(coreExtensionRepository.loadedCoreExtensions()).thenReturn(Stream.of( + new CoreExtension() { + @Override + public String getName() { + return "foo"; + } + + @Override + public void load(Context context) { + context.addExtensions(CeClass.class, ScannerClass.class, WebServerClass.class, + NoAnnotationClass.class, OtherAnnotationClass.class, MultipleAnnotationClass.class); + } + })); + ComponentContainer container = new ComponentContainer(); + + underTest.install(container, noExtensionFilter(), noAdditionalSideFilter()); + + assertThat(container.getPicoContainer().getComponentAdapters()) + .hasSize(ComponentContainer.COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 2); + assertThat(container.getComponentByType(WebServerClass.class)).isNotNull(); + assertThat(container.getComponentByType(MultipleAnnotationClass.class)).isNotNull(); + } + + @ComputeEngineSide + public static final class CeClass { + + } + + @ServerSide + public static final class WebServerClass { + + } + + @ScannerSide + public static final class ScannerClass { + + } + + @ServerSide + @ComputeEngineSide + @ScannerSide + public static final class MultipleAnnotationClass { + + } + + public static final class NoAnnotationClass { + + } + + @DarkSide + public static final class OtherAnnotationClass { + + } + + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + public @interface DarkSide { + } + +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/CheckDatabaseCharsetAtStartupTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/CheckDatabaseCharsetAtStartupTest.java new file mode 100644 index 00000000000..97ed8c9894a --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/CheckDatabaseCharsetAtStartupTest.java @@ -0,0 +1,68 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.db; + +import org.junit.After; +import org.junit.Test; +import org.sonar.api.platform.ServerUpgradeStatus; +import org.sonar.server.platform.db.migration.charset.DatabaseCharsetChecker; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class CheckDatabaseCharsetAtStartupTest { + + private ServerUpgradeStatus upgradeStatus = mock(ServerUpgradeStatus.class); + private DatabaseCharsetChecker charsetChecker = mock(DatabaseCharsetChecker.class); + private CheckDatabaseCharsetAtStartup underTest = new CheckDatabaseCharsetAtStartup(upgradeStatus, charsetChecker); + + @After + public void tearDown() { + underTest.stop(); + } + + @Test + public void test_fresh_install() { + when(upgradeStatus.isFreshInstall()).thenReturn(true); + + underTest.start(); + + verify(charsetChecker).check(DatabaseCharsetChecker.State.FRESH_INSTALL); + } + + @Test + public void test_upgrade() { + when(upgradeStatus.isUpgraded()).thenReturn(true); + + underTest.start(); + + verify(charsetChecker).check(DatabaseCharsetChecker.State.UPGRADE); + } + + @Test + public void test_regular_startup() { + when(upgradeStatus.isFreshInstall()).thenReturn(false); + + underTest.start(); + + verify(charsetChecker).check(DatabaseCharsetChecker.State.STARTUP); + } +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/EmbeddedDatabaseFactoryTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/EmbeddedDatabaseFactoryTest.java new file mode 100644 index 00000000000..7e30958d539 --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/EmbeddedDatabaseFactoryTest.java @@ -0,0 +1,71 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.db; + +import org.junit.Test; +import org.sonar.api.config.internal.MapSettings; +import org.sonar.api.utils.System2; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.sonar.process.ProcessProperties.Property.JDBC_URL; + +public class EmbeddedDatabaseFactoryTest { + + private MapSettings settings = new MapSettings(); + private System2 system2 = mock(System2.class); + + @Test + public void should_start_and_stop_tcp_h2_database() { + settings.setProperty(JDBC_URL.getKey(), "jdbc:h2:tcp:localhost"); + + EmbeddedDatabase embeddedDatabase = mock(EmbeddedDatabase.class); + + EmbeddedDatabaseFactory databaseFactory = new EmbeddedDatabaseFactory(settings.asConfig(), system2) { + @Override + EmbeddedDatabase createEmbeddedDatabase() { + return embeddedDatabase; + } + }; + databaseFactory.start(); + databaseFactory.stop(); + + verify(embeddedDatabase).start(); + verify(embeddedDatabase).stop(); + } + + @Test + public void should_not_start_mem_h2_database() { + settings.setProperty(JDBC_URL.getKey(), "jdbc:h2:mem"); + + EmbeddedDatabase embeddedDatabase = mock(EmbeddedDatabase.class); + + EmbeddedDatabaseFactory databaseFactory = new EmbeddedDatabaseFactory(settings.asConfig(), system2) { + @Override + EmbeddedDatabase createEmbeddedDatabase() { + return embeddedDatabase; + } + }; + databaseFactory.start(); + + verify(embeddedDatabase, never()).start(); + } +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/EmbeddedDatabaseTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/EmbeddedDatabaseTest.java new file mode 100644 index 00000000000..5672b7b60cf --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/EmbeddedDatabaseTest.java @@ -0,0 +1,168 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.db; + +import java.io.IOException; +import java.net.InetAddress; +import java.sql.DriverManager; +import org.h2.Driver; +import org.junit.After; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.DisableOnDebug; +import org.junit.rules.ExpectedException; +import org.junit.rules.TemporaryFolder; +import org.junit.rules.TestRule; +import org.junit.rules.Timeout; +import org.sonar.api.config.internal.MapSettings; +import org.sonar.api.utils.System2; +import org.sonar.api.utils.log.LogTester; +import org.sonar.process.NetworkUtilsImpl; + +import static junit.framework.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.sonar.process.ProcessProperties.Property.JDBC_EMBEDDED_PORT; +import static org.sonar.process.ProcessProperties.Property.JDBC_PASSWORD; +import static org.sonar.process.ProcessProperties.Property.JDBC_URL; +import static org.sonar.process.ProcessProperties.Property.JDBC_USERNAME; +import static org.sonar.process.ProcessProperties.Property.PATH_DATA; + +public class EmbeddedDatabaseTest { + + private static final String LOOPBACK_ADDRESS = InetAddress.getLoopbackAddress().getHostAddress(); + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + @Rule + public LogTester logTester = new LogTester(); + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + @Rule + public TestRule safeguardTimeout = new DisableOnDebug(Timeout.seconds(60)); + + private MapSettings settings = new MapSettings(); + private System2 system2 = mock(System2.class); + private EmbeddedDatabase underTest = new EmbeddedDatabase(settings.asConfig(), system2); + + @After + public void tearDown() { + if (underTest != null) { + underTest.stop(); + } + } + + @Test + public void start_fails_with_IAE_if_property_Data_Path_is_not_set() { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Missing property " + PATH_DATA.getKey()); + + underTest.start(); + } + + @Test + public void start_fails_with_IAE_if_property_Data_Path_is_empty() { + settings.setProperty(PATH_DATA.getKey(), ""); + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Missing property " + PATH_DATA.getKey()); + + underTest.start(); + } + + @Test + public void start_fails_with_IAE_if_JDBC_URL_settings_is_not_set() throws IOException { + settings.setProperty(PATH_DATA.getKey(), temporaryFolder.newFolder().getAbsolutePath()); + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Missing property " + JDBC_URL.getKey()); + + underTest.start(); + } + + @Test + public void start_fails_with_IAE_if_embedded_port_settings_is_not_set() throws IOException { + settings + .setProperty(PATH_DATA.getKey(), temporaryFolder.newFolder().getAbsolutePath()) + .setProperty(JDBC_URL.getKey(), "jdbc url"); + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Missing property " + JDBC_EMBEDDED_PORT.getKey()); + + underTest.start(); + } + + @Test + public void start_ignores_URL_to_create_database_and_uses_empty_username_and_password_when_then_are_not_set() throws IOException { + int port = NetworkUtilsImpl.INSTANCE.getNextAvailablePort(InetAddress.getLoopbackAddress()); + settings + .setProperty(PATH_DATA.getKey(), temporaryFolder.newFolder().getAbsolutePath()) + .setProperty(JDBC_URL.getKey(), "jdbc url") + .setProperty(JDBC_EMBEDDED_PORT.getKey(), "" + port); + + underTest.start(); + + checkDbIsUp(port, "", ""); + } + + @Test + public void start_creates_db_and_adds_tcp_listener() throws IOException { + int port = NetworkUtilsImpl.INSTANCE.getNextAvailablePort(InetAddress.getLoopbackAddress()); + settings + .setProperty(PATH_DATA.getKey(), temporaryFolder.newFolder().getAbsolutePath()) + .setProperty(JDBC_URL.getKey(), "jdbc url") + .setProperty(JDBC_EMBEDDED_PORT.getKey(), "" + port) + .setProperty(JDBC_USERNAME.getKey(), "foo") + .setProperty(JDBC_PASSWORD.getKey(), "bar"); + + underTest.start(); + + checkDbIsUp(port, "foo", "bar"); + + // H2 listens on loopback address only + verify(system2).setProperty("h2.bindAddress", LOOPBACK_ADDRESS); + } + + @Test + public void start_supports_in_memory_H2_JDBC_URL() throws IOException { + int port = NetworkUtilsImpl.INSTANCE.getNextAvailablePort(InetAddress.getLoopbackAddress()); + settings + .setProperty(PATH_DATA.getKey(), temporaryFolder.newFolder().getAbsolutePath()) + .setProperty(JDBC_URL.getKey(), "jdbc:h2:mem:sonar") + .setProperty(JDBC_EMBEDDED_PORT.getKey(), "" + port) + .setProperty(JDBC_USERNAME.getKey(), "foo") + .setProperty(JDBC_PASSWORD.getKey(), "bar"); + + underTest.start(); + + checkDbIsUp(port, "foo", "bar"); + } + + private void checkDbIsUp(int port, String user, String password) { + try { + String driverUrl = String.format("jdbc:h2:tcp://%s:%d/sonar;USER=%s;PASSWORD=%s", LOOPBACK_ADDRESS, port, user, password); + DriverManager.registerDriver(new Driver()); + DriverManager.getConnection(driverUrl).close(); + } catch (Exception ex) { + fail("Unable to connect after start"); + } + } + +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/migration/AutoDbMigrationTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/migration/AutoDbMigrationTest.java new file mode 100644 index 00000000000..1c8d50cd7f5 --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/migration/AutoDbMigrationTest.java @@ -0,0 +1,136 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.db.migration; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.mockito.Mockito; +import org.sonar.api.utils.log.LogTester; +import org.sonar.api.utils.log.LoggerLevel; +import org.sonar.db.DbClient; +import org.sonar.db.dialect.Dialect; +import org.sonar.db.dialect.H2; +import org.sonar.db.dialect.MsSql; +import org.sonar.db.dialect.Oracle; +import org.sonar.db.dialect.PostgreSql; +import org.sonar.server.platform.DefaultServerUpgradeStatus; +import org.sonar.server.platform.db.migration.engine.MigrationEngine; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +public class AutoDbMigrationTest { + @Rule + public ExpectedException thrown = ExpectedException.none(); + @Rule + public LogTester logTester = new LogTester(); + + private DbClient dbClient = mock(DbClient.class, Mockito.RETURNS_DEEP_STUBS); + private DefaultServerUpgradeStatus serverUpgradeStatus = mock(DefaultServerUpgradeStatus.class); + private MigrationEngine migrationEngine = mock(MigrationEngine.class); + private AutoDbMigration underTest = new AutoDbMigration(serverUpgradeStatus, migrationEngine); + + @Test + public void start_runs_MigrationEngine_on_h2_if_fresh_install() { + start_runs_MigrationEngine_for_dialect_if_fresh_install(new H2()); + } + + @Test + public void start_runs_MigrationEngine_on_postgre_if_fresh_install() { + start_runs_MigrationEngine_for_dialect_if_fresh_install(new PostgreSql()); + } + + @Test + public void start_runs_MigrationEngine_on_Oracle_if_fresh_install() { + start_runs_MigrationEngine_for_dialect_if_fresh_install(new Oracle()); + } + + @Test + public void start_runs_MigrationEngine_on_MsSQL_if_fresh_install() { + start_runs_MigrationEngine_for_dialect_if_fresh_install(new MsSql()); + } + + private void start_runs_MigrationEngine_for_dialect_if_fresh_install(Dialect dialect) { + mockDialect(dialect); + mockFreshInstall(true); + + underTest.start(); + + verify(migrationEngine).execute(); + verifyInfoLog(); + } + + @Test + public void start_does_nothing_if_not_fresh_install() { + mockFreshInstall(false); + + underTest.start(); + + verifyZeroInteractions(migrationEngine); + assertThat(logTester.logs(LoggerLevel.INFO)).isEmpty(); + } + + @Test + public void start_runs_MigrationEngine_if_blue_green_upgrade() { + mockFreshInstall(false); + when(serverUpgradeStatus.isUpgraded()).thenReturn(true); + when(serverUpgradeStatus.isBlueGreen()).thenReturn(true); + + underTest.start(); + + verify(migrationEngine).execute(); + assertThat(logTester.logs(LoggerLevel.INFO)).contains("Automatically perform DB migration on blue/green deployment"); + } + + @Test + public void start_does_nothing_if_blue_green_but_no_upgrade() { + mockFreshInstall(false); + when(serverUpgradeStatus.isUpgraded()).thenReturn(false); + when(serverUpgradeStatus.isBlueGreen()).thenReturn(true); + + underTest.start(); + + verifyZeroInteractions(migrationEngine); + assertThat(logTester.logs(LoggerLevel.INFO)).isEmpty(); + } + + @Test + public void stop_has_no_effect() { + underTest.stop(); + } + + private void mockFreshInstall(boolean value) { + when(serverUpgradeStatus.isFreshInstall()).thenReturn(value); + } + + private void mockDialect(Dialect dialect) { + when(dbClient.getDatabase().getDialect()).thenReturn(dialect); + } + + private void verifyInfoLog() { + assertThat(logTester.logs()).hasSize(1); + assertThat(logTester.logs(LoggerLevel.INFO)).containsExactly("Automatically perform DB migration on fresh install"); + } + +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/migration/DatabaseMigrationExecutorServiceAdaptor.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/migration/DatabaseMigrationExecutorServiceAdaptor.java new file mode 100644 index 00000000000..736d056c22b --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/migration/DatabaseMigrationExecutorServiceAdaptor.java @@ -0,0 +1,100 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.db.migration; + +import java.util.Collection; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * Adaptor for the DatabaseMigrationExecutorService interface which implementation of methods all throw + * UnsupportedOperationException. + */ +class DatabaseMigrationExecutorServiceAdaptor implements DatabaseMigrationExecutorService { + + @Override + public void execute(Runnable command) { + throw new UnsupportedOperationException(); + } + + @Override + public void shutdown() { + throw new UnsupportedOperationException(); + } + + @Override + public List<Runnable> shutdownNow() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isShutdown() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isTerminated() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) { + throw new UnsupportedOperationException(); + } + + @Override + public <T> Future<T> submit(Callable<T> task) { + throw new UnsupportedOperationException(); + } + + @Override + public <T> Future<T> submit(Runnable task, T result) { + throw new UnsupportedOperationException(); + } + + @Override + public Future<?> submit(Runnable task) { + throw new UnsupportedOperationException(); + } + + @Override + public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) { + throw new UnsupportedOperationException(); + } + + @Override + public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) { + throw new UnsupportedOperationException(); + } + + @Override + public <T> T invokeAny(Collection<? extends Callable<T>> tasks) { + throw new UnsupportedOperationException(); + } + + @Override + public <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) { + throw new UnsupportedOperationException(); + } +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/migration/DatabaseMigrationImplAsynchronousTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/migration/DatabaseMigrationImplAsynchronousTest.java new file mode 100644 index 00000000000..693a4468fb7 --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/migration/DatabaseMigrationImplAsynchronousTest.java @@ -0,0 +1,53 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.db.migration; + +import org.junit.Test; +import org.sonar.server.platform.Platform; +import org.sonar.server.platform.db.migration.engine.MigrationEngine; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +public class DatabaseMigrationImplAsynchronousTest { + + private boolean taskSuppliedForAsyncProcess = false; + /** + * Implementation of execute wraps specified Runnable to add a delay of 200 ms before passing it + * to a SingleThread executor to execute asynchronously. + */ + private DatabaseMigrationExecutorService executorService = new DatabaseMigrationExecutorServiceAdaptor() { + @Override + public void execute(final Runnable command) { + taskSuppliedForAsyncProcess = true; + } + }; + private MutableDatabaseMigrationState migrationState = mock(MutableDatabaseMigrationState.class); + private Platform platform = mock(Platform.class); + private MigrationEngine migrationEngine = mock(MigrationEngine.class); + private DatabaseMigrationImpl underTest = new DatabaseMigrationImpl(executorService, migrationState, migrationEngine, platform); + + @Test + public void testName() { + underTest.startIt(); + + assertThat(taskSuppliedForAsyncProcess).isTrue(); + } +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/migration/DatabaseMigrationImplConcurrentAccessTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/migration/DatabaseMigrationImplConcurrentAccessTest.java new file mode 100644 index 00000000000..dd9be0581f8 --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/migration/DatabaseMigrationImplConcurrentAccessTest.java @@ -0,0 +1,99 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.db.migration; + +import com.google.common.base.Throwables; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.After; +import org.junit.Test; +import org.sonar.server.platform.Platform; +import org.sonar.server.platform.db.migration.engine.MigrationEngine; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +public class DatabaseMigrationImplConcurrentAccessTest { + + private ExecutorService pool = Executors.newFixedThreadPool(2); + /** + * Latch is used to make sure both testing threads try and call {@link DatabaseMigrationImpl#startIt()} at the + * same time + */ + private CountDownLatch latch = new CountDownLatch(2); + + /** + * Implementation of execute runs Runnable synchronously + */ + private DatabaseMigrationExecutorService executorService = new DatabaseMigrationExecutorServiceAdaptor() { + @Override + public void execute(Runnable command) { + command.run(); + } + }; + private AtomicInteger triggerCount = new AtomicInteger(); + private MigrationEngine incrementingMigrationEngine = new MigrationEngine() { + @Override + public void execute() { + // need execute to consume some time to avoid UT to fail because it ran too fast and threads never executed concurrently + try { + Thread.sleep(200); + } catch (InterruptedException e) { + Throwables.propagate(e); + } + triggerCount.incrementAndGet(); + } + }; + private MutableDatabaseMigrationState migrationState = mock(MutableDatabaseMigrationState.class); + private Platform platform = mock(Platform.class); + private DatabaseMigrationImpl underTest = new DatabaseMigrationImpl(executorService, migrationState, incrementingMigrationEngine, platform); + + @After + public void tearDown() { + pool.shutdownNow(); + } + + @Test + public void two_concurrent_calls_to_startit_call_migration_engine_only_once() throws Exception { + pool.submit(new CallStartit()); + pool.submit(new CallStartit()); + + pool.awaitTermination(2, TimeUnit.SECONDS); + + assertThat(triggerCount.get()).isEqualTo(1); + } + + private class CallStartit implements Runnable { + @Override + public void run() { + latch.countDown(); + try { + latch.await(); + } catch (InterruptedException e) { + // propagate interruption + Thread.currentThread().interrupt(); + } + underTest.startIt(); + } + } +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/migration/DatabaseMigrationImplTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/migration/DatabaseMigrationImplTest.java new file mode 100644 index 00000000000..d4357906db3 --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/migration/DatabaseMigrationImplTest.java @@ -0,0 +1,112 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.db.migration; + +import java.util.Date; +import org.junit.Test; +import org.mockito.InOrder; +import org.sonar.server.platform.Platform; +import org.sonar.server.platform.db.migration.engine.MigrationEngine; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; + +/** + * Unit test for DatabaseMigrationImpl which does not test any of its concurrency management and asynchronous execution code. + */ +public class DatabaseMigrationImplTest { + private static final Throwable AN_ERROR = new RuntimeException("runtime exception created on purpose"); + + /** + * Implementation of execute runs Runnable synchronously. + */ + private DatabaseMigrationExecutorService executorService = new DatabaseMigrationExecutorServiceAdaptor() { + @Override + public void execute(Runnable command) { + command.run(); + } + }; + private MutableDatabaseMigrationState migrationState = new DatabaseMigrationStateImpl(); + private Platform platform = mock(Platform.class); + private MigrationEngine migrationEngine = mock(MigrationEngine.class); + private InOrder inOrder = inOrder(platform, migrationEngine); + + private DatabaseMigrationImpl underTest = new DatabaseMigrationImpl(executorService, migrationState, migrationEngine, platform); + + @Test + public void startit_calls_MigrationEngine_execute() { + underTest.startIt(); + + inOrder.verify(migrationEngine).execute(); + inOrder.verify(platform).doStart(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void status_is_SUCCEEDED_and_failure_is_null_when_trigger_runs_without_an_exception() { + underTest.startIt(); + + assertThat(migrationState.getStatus()).isEqualTo(DatabaseMigrationState.Status.SUCCEEDED); + assertThat(migrationState.getError()).isNull(); + assertThat(migrationState.getStartedAt()).isNotNull(); + } + + @Test + public void status_is_FAILED_and_failure_stores_the_exception_when_trigger_throws_an_exception() { + mockMigrationThrowsError(); + + underTest.startIt(); + + assertThat(migrationState.getStatus()).isEqualTo(DatabaseMigrationState.Status.FAILED); + assertThat(migrationState.getError()).isSameAs(AN_ERROR); + assertThat(migrationState.getStartedAt()).isNotNull(); + } + + @Test + public void successive_calls_to_startIt_reset_status_startedAt_and_failureError() { + mockMigrationThrowsError(); + + underTest.startIt(); + + assertThat(migrationState.getStatus()).isEqualTo(DatabaseMigrationState.Status.FAILED); + assertThat(migrationState.getError()).isSameAs(AN_ERROR); + Date firstStartDate = migrationState.getStartedAt(); + assertThat(firstStartDate).isNotNull(); + + mockMigrationDoesNothing(); + + underTest.startIt(); + + assertThat(migrationState.getStatus()).isEqualTo(DatabaseMigrationState.Status.SUCCEEDED); + assertThat(migrationState.getError()).isNull(); + assertThat(migrationState.getStartedAt()).isNotSameAs(firstStartDate); + } + + private void mockMigrationThrowsError() { + doThrow(AN_ERROR).when(migrationEngine).execute(); + } + + private void mockMigrationDoesNothing() { + doNothing().when(migrationEngine).execute(); + } +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/BaseSectionMBeanTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/BaseSectionMBeanTest.java new file mode 100644 index 00000000000..7cd99dedd22 --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/BaseSectionMBeanTest.java @@ -0,0 +1,61 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.monitoring; + +import java.lang.management.ManagementFactory; +import javax.annotation.CheckForNull; +import javax.management.InstanceNotFoundException; +import javax.management.ObjectInstance; +import javax.management.ObjectName; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class BaseSectionMBeanTest { + + private FakeSection underTest = new FakeSection(); + + @Test + public void test_registration() throws Exception { + assertThat(getMBean()).isNull(); + + underTest.start(); + assertThat(getMBean()).isNotNull(); + + underTest.stop(); + assertThat(getMBean()).isNull(); + } + + @Test + public void do_not_fail_when_stopping_unstarted() throws Exception { + underTest.stop(); + assertThat(getMBean()).isNull(); + } + + @CheckForNull + private ObjectInstance getMBean() throws Exception { + try { + return ManagementFactory.getPlatformMBeanServer().getObjectInstance(new ObjectName(underTest.objectName())); + } catch (InstanceNotFoundException e) { + return null; + } + } + +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/DbConnectionSectionTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/DbConnectionSectionTest.java new file mode 100644 index 00000000000..e68ca345091 --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/DbConnectionSectionTest.java @@ -0,0 +1,70 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.monitoring; + +import org.junit.Rule; +import org.junit.Test; +import org.sonar.api.SonarQubeSide; +import org.sonar.api.SonarRuntime; +import org.sonar.api.utils.System2; +import org.sonar.db.DbTester; +import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo; +import org.sonar.server.platform.db.migration.version.DatabaseVersion; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.sonar.process.systeminfo.SystemInfoUtils.attribute; + +public class DbConnectionSectionTest { + + @Rule + public DbTester dbTester = DbTester.create(System2.INSTANCE); + + private DatabaseVersion databaseVersion = mock(DatabaseVersion.class); + private SonarRuntime runtime = mock(SonarRuntime.class); + private DbConnectionSection underTest = new DbConnectionSection(databaseVersion, dbTester.getDbClient(), runtime); + + @Test + public void jmx_name_is_not_empty() { + assertThat(underTest.name()).isEqualTo("Database"); + } + + @Test + public void pool_info() { + ProtobufSystemInfo.Section section = underTest.toProtobuf(); + assertThat(attribute(section, "Pool Max Connections").getLongValue()).isGreaterThan(0L); + assertThat(attribute(section, "Pool Idle Connections").getLongValue()).isGreaterThanOrEqualTo(0L); + assertThat(attribute(section, "Pool Min Idle Connections").getLongValue()).isGreaterThanOrEqualTo(0L); + assertThat(attribute(section, "Pool Max Idle Connections").getLongValue()).isGreaterThanOrEqualTo(0L); + assertThat(attribute(section, "Pool Max Wait (ms)")).isNotNull(); + assertThat(attribute(section, "Pool Remove Abandoned")).isNotNull(); + assertThat(attribute(section, "Pool Remove Abandoned Timeout (seconds)").getLongValue()).isGreaterThanOrEqualTo(0L); + } + + @Test + public void section_name_depends_on_runtime_side() { + when(runtime.getSonarQubeSide()).thenReturn(SonarQubeSide.COMPUTE_ENGINE); + assertThat(underTest.toProtobuf().getName()).isEqualTo("Compute Engine Database Connection"); + + when(runtime.getSonarQubeSide()).thenReturn(SonarQubeSide.SERVER ); + assertThat(underTest.toProtobuf().getName()).isEqualTo("Web Database Connection"); + } +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/EsIndexesSectionTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/EsIndexesSectionTest.java new file mode 100644 index 00000000000..d82b90a7319 --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/EsIndexesSectionTest.java @@ -0,0 +1,86 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.monitoring; + +import org.elasticsearch.ElasticsearchException; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo; +import org.sonar.server.es.EsClient; +import org.sonar.server.es.EsTester; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.sonar.process.systeminfo.SystemInfoUtils.attribute; +import static org.sonar.server.platform.monitoring.SystemInfoTesting.assertThatAttributeIs; + +public class EsIndexesSectionTest { + + @Rule + public EsTester es = EsTester.create(); + + private EsIndexesSection underTest = new EsIndexesSection(es.client()); + + @Test + public void name() { + assertThat(underTest.toProtobuf().getName()).isEqualTo("Search Indexes"); + } + + @Test + public void index_attributes() { + ProtobufSystemInfo.Section section = underTest.toProtobuf(); + + // one index "issues" + assertThat(attribute(section, "Index issues - Docs").getLongValue()).isEqualTo(0L); + assertThat(attribute(section, "Index issues - Shards").getLongValue()).isGreaterThan(0); + assertThat(attribute(section, "Index issues - Store Size").getStringValue()).isNotNull(); + } + + @Test + public void attributes_displays_exception_message_when_cause_null_when_client_fails() { + EsClient esClientMock = mock(EsClient.class); + EsIndexesSection underTest = new EsIndexesSection(esClientMock); + when(esClientMock.prepareStats()).thenThrow(new RuntimeException("RuntimeException with no cause")); + + ProtobufSystemInfo.Section section = underTest.toProtobuf(); + assertThatAttributeIs(section, "Error", "RuntimeException with no cause"); + } + + @Test + public void attributes_displays_exception_message_when_cause_is_not_ElasticSearchException_when_client_fails() { + EsClient esClientMock = mock(EsClient.class); + EsIndexesSection underTest = new EsIndexesSection(esClientMock); + when(esClientMock.prepareStats()).thenThrow(new RuntimeException("RuntimeException with cause not ES", new IllegalArgumentException("some cause message"))); + + ProtobufSystemInfo.Section section = underTest.toProtobuf(); + assertThatAttributeIs(section, "Error", "RuntimeException with cause not ES"); + } + + @Test + public void attributes_displays_cause_message_when_cause_is_ElasticSearchException_when_client_fails() { + EsClient esClientMock = mock(EsClient.class); + EsIndexesSection underTest = new EsIndexesSection(esClientMock); + when(esClientMock.prepareStats()).thenThrow(new RuntimeException("RuntimeException with ES cause", new ElasticsearchException("some cause message"))); + + ProtobufSystemInfo.Section section = underTest.toProtobuf(); + assertThatAttributeIs(section, "Error", "some cause message"); + } +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/EsStateSectionTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/EsStateSectionTest.java new file mode 100644 index 00000000000..3a03fc5a696 --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/EsStateSectionTest.java @@ -0,0 +1,91 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.monitoring; + +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.cluster.health.ClusterHealthStatus; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo; +import org.sonar.server.es.EsClient; +import org.sonar.server.es.EsTester; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.sonar.process.systeminfo.SystemInfoUtils.attribute; +import static org.sonar.server.platform.monitoring.SystemInfoTesting.assertThatAttributeIs; + +public class EsStateSectionTest { + + @Rule + public EsTester es = EsTester.create(); + + private EsStateSection underTest = new EsStateSection(es.client()); + + @Test + public void name() { + assertThat(underTest.toProtobuf().getName()).isEqualTo("Search State"); + } + + @Test + public void es_state() { + assertThatAttributeIs(underTest.toProtobuf(), "State", ClusterHealthStatus.GREEN.name()); + } + + @Test + public void node_attributes() { + ProtobufSystemInfo.Section section = underTest.toProtobuf(); + assertThat(attribute(section, "CPU Usage (%)")).isNotNull(); + assertThat(attribute(section, "Disk Available")).isNotNull(); + assertThat(attribute(section, "Store Size")).isNotNull(); + assertThat(attribute(section, "Translog Size")).isNotNull(); + } + + @Test + public void attributes_displays_exception_message_when_cause_null_when_client_fails() { + EsClient esClientMock = mock(EsClient.class); + EsStateSection underTest = new EsStateSection(esClientMock); + when(esClientMock.prepareClusterStats()).thenThrow(new RuntimeException("RuntimeException with no cause")); + + ProtobufSystemInfo.Section section = underTest.toProtobuf(); + assertThatAttributeIs(section, "State", "RuntimeException with no cause"); + } + + @Test + public void attributes_displays_exception_message_when_cause_is_not_ElasticSearchException_when_client_fails() { + EsClient esClientMock = mock(EsClient.class); + EsStateSection underTest = new EsStateSection(esClientMock); + when(esClientMock.prepareClusterStats()).thenThrow(new RuntimeException("RuntimeException with cause not ES", new IllegalArgumentException("some cause message"))); + + ProtobufSystemInfo.Section section = underTest.toProtobuf(); + assertThatAttributeIs(section, "State", "RuntimeException with cause not ES"); + } + + @Test + public void attributes_displays_cause_message_when_cause_is_ElasticSearchException_when_client_fails() { + EsClient esClientMock = mock(EsClient.class); + EsStateSection underTest = new EsStateSection(esClientMock); + when(esClientMock.prepareClusterStats()).thenThrow(new RuntimeException("RuntimeException with ES cause", new ElasticsearchException("some cause message"))); + + ProtobufSystemInfo.Section section = underTest.toProtobuf(); + assertThatAttributeIs(section, "State", "some cause message"); + } +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/FakeSection.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/FakeSection.java new file mode 100644 index 00000000000..bf3c6e01ed0 --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/FakeSection.java @@ -0,0 +1,40 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.monitoring; + +import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo; + +public class FakeSection extends BaseSectionMBean implements FakeSectionMBean { + + @Override + public int getFake() { + return 42; + } + + @Override + public String name() { + return "fake"; + } + + @Override + public ProtobufSystemInfo.Section toProtobuf() { + return ProtobufSystemInfo.Section.newBuilder().build(); + } +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/FakeSectionMBean.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/FakeSectionMBean.java new file mode 100644 index 00000000000..5879452629e --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/FakeSectionMBean.java @@ -0,0 +1,24 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.monitoring; + +public interface FakeSectionMBean { + int getFake(); +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/PluginsSectionTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/PluginsSectionTest.java new file mode 100644 index 00000000000..5b6837473e4 --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/PluginsSectionTest.java @@ -0,0 +1,62 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.monitoring; + +import java.util.Arrays; +import org.junit.Test; +import org.sonar.core.platform.PluginInfo; +import org.sonar.core.platform.PluginRepository; +import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo; +import org.sonar.updatecenter.common.Version; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.sonar.server.platform.monitoring.SystemInfoTesting.assertThatAttributeIs; + +public class PluginsSectionTest { + + private PluginRepository repo = mock(PluginRepository.class); + private PluginsSection underTest = new PluginsSection(repo); + + @Test + public void name() { + assertThat(underTest.toProtobuf().getName()).isEqualTo("Plugins"); + } + + @Test + public void plugin_name_and_version() { + when(repo.getPluginInfos()).thenReturn(Arrays.asList( + new PluginInfo("key-1") + .setName("Plugin 1") + .setVersion(Version.create("1.1")), + new PluginInfo("key-2") + .setName("Plugin 2") + .setVersion(Version.create("2.2")), + new PluginInfo("no-version") + .setName("No Version"))); + + ProtobufSystemInfo.Section section = underTest.toProtobuf(); + + assertThatAttributeIs(section, "key-1", "1.1 [Plugin 1]"); + assertThatAttributeIs(section, "key-2", "2.2 [Plugin 2]"); + assertThatAttributeIs(section, "no-version", "[No Version]"); + } +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/SettingsSectionTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/SettingsSectionTest.java new file mode 100644 index 00000000000..5860ed73671 --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/SettingsSectionTest.java @@ -0,0 +1,100 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.monitoring; + +import org.junit.Test; +import org.sonar.api.PropertyType; +import org.sonar.api.config.PropertyDefinition; +import org.sonar.api.config.PropertyDefinitions; +import org.sonar.api.config.Settings; +import org.sonar.api.config.internal.MapSettings; +import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo; + +import static org.apache.commons.lang.StringUtils.repeat; +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.process.systeminfo.SystemInfoUtils.attribute; +import static org.sonar.server.platform.monitoring.SystemInfoTesting.assertThatAttributeIs; + +public class SettingsSectionTest { + + private static final String PASSWORD_PROPERTY = "sonar.password"; + + private PropertyDefinitions defs = new PropertyDefinitions(PropertyDefinition.builder(PASSWORD_PROPERTY).type(PropertyType.PASSWORD).build()); + private Settings settings = new MapSettings(defs); + private SettingsSection underTest = new SettingsSection(settings); + + @Test + public void return_properties_and_sort_by_key() { + settings.setProperty("foo", "foo value"); + settings.setProperty("bar", "bar value"); + + ProtobufSystemInfo.Section protobuf = underTest.toProtobuf(); + assertThatAttributeIs(protobuf, "bar", "bar value"); + assertThatAttributeIs(protobuf, "foo", "foo value"); + + // keys are ordered alphabetically + assertThat(protobuf.getAttributesList()) + .extracting(ProtobufSystemInfo.Attribute::getKey) + .containsExactly("bar", "foo"); + } + + @Test + public void truncate_long_property_values() { + settings.setProperty("foo", repeat("abcde", 1_000)); + + ProtobufSystemInfo.Section protobuf = underTest.toProtobuf(); + String value = attribute(protobuf, "foo").getStringValue(); + assertThat(value).hasSize(500).startsWith("abcde"); + } + + @Test + public void value_is_obfuscated_if_key_matches_patterns() { + verifyObfuscated(PASSWORD_PROPERTY); + verifyObfuscated("foo.password.something"); + // case insensitive search of "password" term + verifyObfuscated("bar.CheckPassword"); + verifyObfuscated("foo.passcode.something"); + // case insensitive search of "passcode" term + verifyObfuscated("bar.CheckPassCode"); + verifyObfuscated("foo.something.secured"); + verifyObfuscated("bar.something.Secured"); + verifyObfuscated("sonar.auth.jwtBase64Hs256Secret"); + + verifyNotObfuscated("securedStuff"); + verifyNotObfuscated("foo"); + } + + private void verifyObfuscated(String key) { + settings.setProperty(key, "foo"); + ProtobufSystemInfo.Section protobuf = underTest.toProtobuf(); + assertThatAttributeIs(protobuf, key, "xxxxxxxx"); + } + + private void verifyNotObfuscated(String key) { + settings.setProperty(key, "foo"); + ProtobufSystemInfo.Section protobuf = underTest.toProtobuf(); + assertThatAttributeIs(protobuf, key, "foo"); + } + + @Test + public void test_monitor_name() { + assertThat(underTest.toProtobuf().getName()).isEqualTo("Settings"); + } +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/StandaloneSystemSectionTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/StandaloneSystemSectionTest.java new file mode 100644 index 00000000000..bc233dab4e9 --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/StandaloneSystemSectionTest.java @@ -0,0 +1,155 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.monitoring; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.api.config.internal.MapSettings; +import org.sonar.api.platform.Server; +import org.sonar.api.security.SecurityRealm; +import org.sonar.api.utils.log.LoggerLevel; +import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo; +import org.sonar.server.authentication.IdentityProviderRepositoryRule; +import org.sonar.server.authentication.TestIdentityProvider; +import org.sonar.server.log.ServerLogging; +import org.sonar.server.platform.OfficialDistribution; +import org.sonar.server.user.SecurityRealmFactory; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.sonar.process.systeminfo.SystemInfoUtils.attribute; +import static org.sonar.server.platform.monitoring.SystemInfoTesting.assertThatAttributeIs; + +public class StandaloneSystemSectionTest { + + private static final String SERVER_ID_PROPERTY = "Server ID"; + private static final String SERVER_ID_VALIDATED_PROPERTY = "Server ID validated"; + + @Rule + public IdentityProviderRepositoryRule identityProviderRepository = new IdentityProviderRepositoryRule(); + + private MapSettings settings = new MapSettings(); + private Server server = mock(Server.class); + private ServerLogging serverLogging = mock(ServerLogging.class); + private SecurityRealmFactory securityRealmFactory = mock(SecurityRealmFactory.class); + private OfficialDistribution officialDistribution = mock(OfficialDistribution.class); + + private StandaloneSystemSection underTest = new StandaloneSystemSection(settings.asConfig(), securityRealmFactory, identityProviderRepository, server, + serverLogging, officialDistribution); + + @Before + public void setUp() throws Exception { + when(serverLogging.getRootLoggerLevel()).thenReturn(LoggerLevel.DEBUG); + } + + @Test + public void name_is_not_empty() { + assertThat(underTest.name()).isNotEmpty(); + } + + @Test + public void test_getServerId() { + when(server.getId()).thenReturn("ABC"); + assertThat(underTest.getServerId()).isEqualTo("ABC"); + } + + @Test + public void official_distribution() { + when(officialDistribution.check()).thenReturn(true); + + ProtobufSystemInfo.Section protobuf = underTest.toProtobuf(); + assertThatAttributeIs(protobuf, "Official Distribution", true); + } + + @Test + public void not_an_official_distribution() { + when(officialDistribution.check()).thenReturn(false); + + ProtobufSystemInfo.Section protobuf = underTest.toProtobuf(); + assertThatAttributeIs(protobuf, "Official Distribution", false); + } + + @Test + public void get_realm() { + SecurityRealm realm = mock(SecurityRealm.class); + when(realm.getName()).thenReturn("LDAP"); + when(securityRealmFactory.getRealm()).thenReturn(realm); + + ProtobufSystemInfo.Section protobuf = underTest.toProtobuf(); + assertThatAttributeIs(protobuf, "External User Authentication", "LDAP"); + } + + @Test + public void no_realm() { + when(securityRealmFactory.getRealm()).thenReturn(null); + + ProtobufSystemInfo.Section protobuf = underTest.toProtobuf(); + assertThat(attribute(protobuf, "External User Authentication")).isNull(); + } + + @Test + public void get_enabled_identity_providers() { + identityProviderRepository.addIdentityProvider(new TestIdentityProvider() + .setKey("github") + .setName("GitHub") + .setEnabled(true)); + identityProviderRepository.addIdentityProvider(new TestIdentityProvider() + .setKey("bitbucket") + .setName("Bitbucket") + .setEnabled(true)); + identityProviderRepository.addIdentityProvider(new TestIdentityProvider() + .setKey("disabled") + .setName("Disabled") + .setEnabled(false)); + + ProtobufSystemInfo.Section protobuf = underTest.toProtobuf(); + assertThatAttributeIs(protobuf, "Accepted external identity providers", "Bitbucket, GitHub"); + } + + @Test + public void get_enabled_identity_providers_allowing_users_to_signup() { + identityProviderRepository.addIdentityProvider(new TestIdentityProvider() + .setKey("github") + .setName("GitHub") + .setEnabled(true) + .setAllowsUsersToSignUp(true)); + identityProviderRepository.addIdentityProvider(new TestIdentityProvider() + .setKey("bitbucket") + .setName("Bitbucket") + .setEnabled(true) + .setAllowsUsersToSignUp(false)); + identityProviderRepository.addIdentityProvider(new TestIdentityProvider() + .setKey("disabled") + .setName("Disabled") + .setEnabled(false) + .setAllowsUsersToSignUp(true)); + + ProtobufSystemInfo.Section protobuf = underTest.toProtobuf(); + assertThatAttributeIs(protobuf, "External identity providers whose users are allowed to sign themselves up", "GitHub"); + } + + @Test + public void return_nb_of_processors() { + ProtobufSystemInfo.Section protobuf = underTest.toProtobuf(); + assertThat(attribute(protobuf, "Processors").getLongValue()).isGreaterThan(0); + } +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/cluster/AppNodesInfoLoaderImplTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/cluster/AppNodesInfoLoaderImplTest.java new file mode 100644 index 00000000000..e67e84b37ed --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/cluster/AppNodesInfoLoaderImplTest.java @@ -0,0 +1,97 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.monitoring.cluster; + +import com.hazelcast.core.Member; +import com.hazelcast.core.MemberSelector; +import com.hazelcast.nio.Address; +import java.io.IOException; +import java.net.InetAddress; +import java.util.Collection; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.mockito.Mockito; +import org.sonar.process.cluster.hz.DistributedAnswer; +import org.sonar.process.cluster.hz.DistributedCall; +import org.sonar.process.cluster.hz.HazelcastMember; +import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo.Section; +import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo.SystemInfo; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + + +public class AppNodesInfoLoaderImplTest { + + private static final InetAddress AN_ADDRESS = InetAddress.getLoopbackAddress(); + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + private HazelcastMember hzMember = mock(HazelcastMember.class); + private AppNodesInfoLoaderImpl underTest = new AppNodesInfoLoaderImpl(hzMember); + + @Test + public void load_info_from_all_nodes() throws Exception { + DistributedAnswer<SystemInfo> answer = new DistributedAnswer<>(); + answer.setAnswer(newMember("foo"), SystemInfo.newBuilder().addSections(Section.newBuilder().build()).build()); + answer.setTimedOut(newMember("bar")); + answer.setFailed(newMember("baz"), new IOException("BOOM")); + when(hzMember.call(any(DistributedCall.class), any(MemberSelector.class), anyLong())).thenReturn(answer); + + Collection<NodeInfo> nodes = underTest.load(); + + assertThat(nodes).hasSize(3); + + NodeInfo successfulNodeInfo = findNode(nodes, "foo"); + assertThat(successfulNodeInfo.getName()).isEqualTo("foo"); + assertThat(successfulNodeInfo.getHost()).hasValue(AN_ADDRESS.getHostAddress()); + assertThat(successfulNodeInfo.getErrorMessage()).isEmpty(); + assertThat(successfulNodeInfo.getSections()).hasSize(1); + + NodeInfo timedOutNodeInfo = findNode(nodes, "bar"); + assertThat(timedOutNodeInfo.getName()).isEqualTo("bar"); + assertThat(timedOutNodeInfo.getErrorMessage()).hasValue("Failed to retrieve information on time"); + assertThat(timedOutNodeInfo.getSections()).isEmpty(); + + NodeInfo failedNodeInfo = findNode(nodes, "baz"); + assertThat(failedNodeInfo.getName()).isEqualTo("baz"); + assertThat(failedNodeInfo.getErrorMessage()).hasValue("Failed to retrieve information: BOOM"); + assertThat(failedNodeInfo.getSections()).isEmpty(); + } + + private NodeInfo findNode(Collection<NodeInfo> nodes, String name) { + return nodes.stream() + .filter(n -> n.getName().equals(name)) + .findFirst() + .orElseThrow(IllegalStateException::new); + } + + private Member newMember(String name) { + Member member = mock(Member.class, Mockito.RETURNS_MOCKS); + when(member.getStringAttribute(HazelcastMember.Attribute.NODE_NAME.getKey())).thenReturn(name); + when(member.getAddress()).thenReturn(new Address(AN_ADDRESS, 6789)); + return member; + } +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/cluster/CeQueueGlobalSectionTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/cluster/CeQueueGlobalSectionTest.java new file mode 100644 index 00000000000..f7d9bf0941f --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/cluster/CeQueueGlobalSectionTest.java @@ -0,0 +1,86 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.monitoring.cluster; + +import java.util.Optional; +import org.junit.Test; +import org.mockito.Mockito; +import org.sonar.ce.configuration.WorkerCountProvider; +import org.sonar.db.DbClient; +import org.sonar.db.ce.CeQueueDto; +import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo; +import org.sonar.server.property.InternalProperties; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.sonar.server.platform.monitoring.SystemInfoTesting.assertThatAttributeIs; + +public class CeQueueGlobalSectionTest { + + private DbClient dbClient = mock(DbClient.class, Mockito.RETURNS_DEEP_STUBS); + private WorkerCountProvider workerCountProvider = mock(WorkerCountProvider.class); + + @Test + public void test_queue_state_with_default_settings() { + when(dbClient.ceQueueDao().countByStatus(any(), eq(CeQueueDto.Status.PENDING))).thenReturn(10); + when(dbClient.ceQueueDao().countByStatus(any(), eq(CeQueueDto.Status.IN_PROGRESS))).thenReturn(1); + + CeQueueGlobalSection underTest = new CeQueueGlobalSection(dbClient); + ProtobufSystemInfo.Section section = underTest.toProtobuf(); + + assertThatAttributeIs(section, "Total Pending", 10); + assertThatAttributeIs(section, "Total In Progress", 1); + assertThatAttributeIs(section, "Max Workers per Node", 1); + } + + @Test + public void test_queue_state_with_overridden_settings() { + when(dbClient.ceQueueDao().countByStatus(any(), eq(CeQueueDto.Status.PENDING))).thenReturn(10); + when(dbClient.ceQueueDao().countByStatus(any(), eq(CeQueueDto.Status.IN_PROGRESS))).thenReturn(2); + when(workerCountProvider.get()).thenReturn(5); + + CeQueueGlobalSection underTest = new CeQueueGlobalSection(dbClient, workerCountProvider); + ProtobufSystemInfo.Section section = underTest.toProtobuf(); + + assertThatAttributeIs(section, "Total Pending", 10); + assertThatAttributeIs(section, "Total In Progress", 2); + assertThatAttributeIs(section, "Max Workers per Node", 5); + } + + @Test + public void test_workers_not_paused() { + CeQueueGlobalSection underTest = new CeQueueGlobalSection(dbClient, workerCountProvider); + ProtobufSystemInfo.Section section = underTest.toProtobuf(); + + assertThatAttributeIs(section, "Workers Paused", false); + } + + @Test + public void test_workers_paused() { + when(dbClient.internalPropertiesDao().selectByKey(any(), eq(InternalProperties.COMPUTE_ENGINE_PAUSE))).thenReturn(Optional.of("true")); + + CeQueueGlobalSection underTest = new CeQueueGlobalSection(dbClient, workerCountProvider); + ProtobufSystemInfo.Section section = underTest.toProtobuf(); + + assertThatAttributeIs(section, "Workers Paused", true); + } +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/cluster/EsClusterStateSectionTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/cluster/EsClusterStateSectionTest.java new file mode 100644 index 00000000000..b82012392b7 --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/cluster/EsClusterStateSectionTest.java @@ -0,0 +1,48 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.monitoring.cluster; + +import org.junit.Rule; +import org.junit.Test; +import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo; +import org.sonar.server.es.EsTester; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.process.systeminfo.SystemInfoUtils.attribute; + +public class EsClusterStateSectionTest { + + @Rule + public EsTester es = EsTester.create(); + + private EsClusterStateSection underTest = new EsClusterStateSection(es.client()); + + @Test + public void test_name() { + assertThat(underTest.toProtobuf().getName()).isEqualTo("Search State"); + } + + @Test + public void test_attributes() { + ProtobufSystemInfo.Section section = underTest.toProtobuf(); + assertThat(attribute(section, "Nodes").getLongValue()).isGreaterThan(0); + assertThat(attribute(section, "State").getStringValue()).isIn("RED", "YELLOW", "GREEN"); + } +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/cluster/GlobalInfoLoaderTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/cluster/GlobalInfoLoaderTest.java new file mode 100644 index 00000000000..442a55427ad --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/cluster/GlobalInfoLoaderTest.java @@ -0,0 +1,44 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.monitoring.cluster; + +import java.util.List; +import org.junit.Test; +import org.sonar.process.systeminfo.SystemInfoSection; +import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo; + +import static org.assertj.core.api.Assertions.assertThat; + +public class GlobalInfoLoaderTest { + + @Test + public void call_only_SystemInfoSection_that_inherit_Global() { + // two globals and one standard + SystemInfoSection[] sections = new SystemInfoSection[] { + new TestGlobalSystemInfoSection("foo"), new TestSystemInfoSection("bar"), new TestGlobalSystemInfoSection("baz")}; + + GlobalInfoLoader underTest = new GlobalInfoLoader(sections); + List<ProtobufSystemInfo.Section> loadedInfo = underTest.load(); + + assertThat(loadedInfo).extracting(ProtobufSystemInfo.Section::getName) + .containsExactlyInAnyOrder("foo", "baz"); + } + +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/cluster/GlobalSystemSectionTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/cluster/GlobalSystemSectionTest.java new file mode 100644 index 00000000000..f12c7461f1e --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/cluster/GlobalSystemSectionTest.java @@ -0,0 +1,113 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.monitoring.cluster; + +import org.junit.Rule; +import org.junit.Test; +import org.sonar.api.config.internal.MapSettings; +import org.sonar.api.platform.Server; +import org.sonar.api.security.SecurityRealm; +import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo; +import org.sonar.server.authentication.IdentityProviderRepositoryRule; +import org.sonar.server.authentication.TestIdentityProvider; +import org.sonar.server.user.SecurityRealmFactory; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.sonar.process.systeminfo.SystemInfoUtils.attribute; +import static org.sonar.server.platform.monitoring.SystemInfoTesting.assertThatAttributeIs; + +public class GlobalSystemSectionTest { + + @Rule + public IdentityProviderRepositoryRule identityProviderRepository = new IdentityProviderRepositoryRule(); + + private MapSettings settings = new MapSettings(); + private Server server = mock(Server.class); + private SecurityRealmFactory securityRealmFactory = mock(SecurityRealmFactory.class); + + private GlobalSystemSection underTest = new GlobalSystemSection(settings.asConfig(), + server, securityRealmFactory, identityProviderRepository); + + @Test + public void name_is_not_empty() { + assertThat(underTest.toProtobuf().getName()).isEqualTo("System"); + } + + @Test + public void get_realm() { + SecurityRealm realm = mock(SecurityRealm.class); + when(realm.getName()).thenReturn("LDAP"); + when(securityRealmFactory.getRealm()).thenReturn(realm); + + ProtobufSystemInfo.Section protobuf = underTest.toProtobuf(); + assertThatAttributeIs(protobuf, "External User Authentication", "LDAP"); + } + + @Test + public void no_realm() { + when(securityRealmFactory.getRealm()).thenReturn(null); + + ProtobufSystemInfo.Section protobuf = underTest.toProtobuf(); + assertThat(attribute(protobuf, "External User Authentication")).isNull(); + } + + @Test + public void get_enabled_identity_providers() { + identityProviderRepository.addIdentityProvider(new TestIdentityProvider() + .setKey("github") + .setName("GitHub") + .setEnabled(true)); + identityProviderRepository.addIdentityProvider(new TestIdentityProvider() + .setKey("bitbucket") + .setName("Bitbucket") + .setEnabled(true)); + identityProviderRepository.addIdentityProvider(new TestIdentityProvider() + .setKey("disabled") + .setName("Disabled") + .setEnabled(false)); + + ProtobufSystemInfo.Section protobuf = underTest.toProtobuf(); + assertThatAttributeIs(protobuf, "Accepted external identity providers", "Bitbucket, GitHub"); + } + + @Test + public void get_enabled_identity_providers_allowing_users_to_signup() { + identityProviderRepository.addIdentityProvider(new TestIdentityProvider() + .setKey("github") + .setName("GitHub") + .setEnabled(true) + .setAllowsUsersToSignUp(true)); + identityProviderRepository.addIdentityProvider(new TestIdentityProvider() + .setKey("bitbucket") + .setName("Bitbucket") + .setEnabled(true) + .setAllowsUsersToSignUp(false)); + identityProviderRepository.addIdentityProvider(new TestIdentityProvider() + .setKey("disabled") + .setName("Disabled") + .setEnabled(false) + .setAllowsUsersToSignUp(true)); + + ProtobufSystemInfo.Section protobuf = underTest.toProtobuf(); + assertThatAttributeIs(protobuf, "External identity providers whose users are allowed to sign themselves up", "GitHub"); + } +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/cluster/NodeInfoTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/cluster/NodeInfoTest.java new file mode 100644 index 00000000000..e0863fee41b --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/cluster/NodeInfoTest.java @@ -0,0 +1,42 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.monitoring.cluster; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class NodeInfoTest { + + @Test + public void test_equals_and_hashCode() { + NodeInfo foo = new NodeInfo("foo"); + NodeInfo bar = new NodeInfo("bar"); + NodeInfo bar2 = new NodeInfo("bar"); + + assertThat(foo.equals(foo)).isTrue(); + assertThat(foo.equals(bar)).isFalse(); + assertThat(bar.equals(bar2)).isTrue(); + + assertThat(bar.hashCode()).isEqualTo(bar.hashCode()); + assertThat(bar.hashCode()).isEqualTo(bar2.hashCode()); + } + +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/cluster/NodeSystemSectionTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/cluster/NodeSystemSectionTest.java new file mode 100644 index 00000000000..9fdb245d475 --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/cluster/NodeSystemSectionTest.java @@ -0,0 +1,105 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.monitoring.cluster; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.config.internal.MapSettings; +import org.sonar.api.platform.Server; +import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo; +import org.sonar.server.platform.OfficialDistribution; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.sonar.process.ProcessProperties.Property.PATH_DATA; +import static org.sonar.process.ProcessProperties.Property.PATH_HOME; +import static org.sonar.process.ProcessProperties.Property.PATH_LOGS; +import static org.sonar.process.ProcessProperties.Property.PATH_TEMP; +import static org.sonar.process.ProcessProperties.Property.PATH_WEB; +import static org.sonar.process.systeminfo.SystemInfoUtils.attribute; +import static org.sonar.server.platform.monitoring.SystemInfoTesting.assertThatAttributeIs; + +public class NodeSystemSectionTest { + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + private MapSettings settings = new MapSettings(); + private Server server = mock(Server.class, RETURNS_DEEP_STUBS); + private OfficialDistribution officialDistrib = mock(OfficialDistribution.class); + private NodeSystemSection underTest = new NodeSystemSection(settings.asConfig(), server, officialDistrib); + + @Test + public void test_section_name() { + ProtobufSystemInfo.Section section = underTest.toProtobuf(); + + assertThat(section.getName()).isEqualTo("System"); + } + + @Test + public void return_server_version() { + when(server.getVersion()).thenReturn("6.6"); + + ProtobufSystemInfo.Section section = underTest.toProtobuf(); + + assertThatAttributeIs(section, "Version", "6.6"); + } + + @Test + public void return_official_distribution_flag() { + when(officialDistrib.check()).thenReturn(true); + + ProtobufSystemInfo.Section section = underTest.toProtobuf(); + + assertThatAttributeIs(section, "Official Distribution", true); + } + + @Test + public void return_nb_of_processors() { + ProtobufSystemInfo.Section section = underTest.toProtobuf(); + + assertThat(attribute(section, "Processors").getLongValue()).isGreaterThan(0); + } + + @Test + public void return_dir_paths() { + settings.setProperty(PATH_HOME.getKey(), "/home"); + settings.setProperty(PATH_DATA.getKey(), "/data"); + settings.setProperty(PATH_TEMP.getKey(), "/temp"); + settings.setProperty(PATH_LOGS.getKey(), "/logs"); + settings.setProperty(PATH_WEB.getKey(), "/web"); + + ProtobufSystemInfo.Section section = underTest.toProtobuf(); + + assertThatAttributeIs(section, "Home Dir", "/home"); + assertThatAttributeIs(section, "Data Dir", "/data"); + assertThatAttributeIs(section, "Temp Dir", "/temp"); + + // logs dir is part of LoggingSection + assertThat(attribute(section, "Logs Dir")).isNull(); + + // for internal usage + assertThat(attribute(section, "Web Dir")).isNull(); + + } +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/cluster/SearchNodesInfoLoaderImplTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/cluster/SearchNodesInfoLoaderImplTest.java new file mode 100644 index 00000000000..37a712ecc47 --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/monitoring/cluster/SearchNodesInfoLoaderImplTest.java @@ -0,0 +1,59 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.monitoring.cluster; + +import java.util.Collection; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo; +import org.sonar.server.es.EsTester; +import org.sonar.server.es.newindex.FakeIndexDefinition; + +import static org.assertj.core.api.Assertions.assertThat; + +public class SearchNodesInfoLoaderImplTest { + + @Rule + public EsTester es = EsTester.createCustom(new FakeIndexDefinition()); + + private SearchNodesInfoLoaderImpl underTest = new SearchNodesInfoLoaderImpl(es.client()); + + @Test + public void return_info_from_elasticsearch_api() { + Collection<NodeInfo> nodes = underTest.load(); + + assertThat(nodes).hasSize(1); + NodeInfo node = nodes.iterator().next(); + assertThat(node.getName()).isNotEmpty(); + assertThat(node.getHost()).isNotEmpty(); + assertThat(node.getSections()).hasSize(1); + ProtobufSystemInfo.Section stateSection = node.getSections().get(0); + + assertThat(stateSection.getAttributesList()) + .extracting(ProtobufSystemInfo.Attribute::getKey) + .contains( + "Disk Available", "Store Size", + "JVM Heap Usage", "JVM Heap Used", "JVM Heap Max", "JVM Non Heap Used", + "JVM Threads", + "Field Data Memory", "Field Data Circuit Breaker Limit", "Field Data Circuit Breaker Estimation", + "Request Circuit Breaker Limit", "Request Circuit Breaker Estimation", + "Query Cache Memory", "Request Cache Memory"); + } +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/serverid/ServerIdFactoryImplTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/serverid/ServerIdFactoryImplTest.java new file mode 100644 index 00000000000..3756ca14b58 --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/serverid/ServerIdFactoryImplTest.java @@ -0,0 +1,119 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.serverid; + +import com.tngtech.java.junit.dataprovider.DataProvider; +import com.tngtech.java.junit.dataprovider.DataProviderRunner; +import com.tngtech.java.junit.dataprovider.UseDataProvider; +import java.text.SimpleDateFormat; +import java.util.Date; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.sonar.api.config.Configuration; +import org.sonar.api.config.internal.MapSettings; +import org.sonar.core.platform.ServerId; +import org.sonar.core.util.UuidFactory; +import org.sonar.core.util.Uuids; + +import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.sonar.core.platform.ServerId.DATABASE_ID_LENGTH; +import static org.sonar.core.platform.ServerId.NOT_UUID_DATASET_ID_LENGTH; +import static org.sonar.core.platform.ServerId.UUID_DATASET_ID_LENGTH; +import static org.sonar.process.ProcessProperties.Property.JDBC_URL; +import static org.sonar.server.platform.serverid.ServerIdFactoryImpl.crc32Hex; + +@RunWith(DataProviderRunner.class) +public class ServerIdFactoryImplTest { + private static final ServerId A_SERVERID = ServerId.of(randomAlphabetic(DATABASE_ID_LENGTH), randomAlphabetic(UUID_DATASET_ID_LENGTH)); + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + private MapSettings settings = new MapSettings(); + private Configuration config = settings.asConfig(); + private UuidFactory uuidFactory = mock(UuidFactory.class); + private JdbcUrlSanitizer jdbcUrlSanitizer = mock(JdbcUrlSanitizer.class); + private ServerIdFactoryImpl underTest = new ServerIdFactoryImpl(config, uuidFactory, jdbcUrlSanitizer); + + @Test + public void create_from_scratch_fails_with_ISE_if_JDBC_property_not_set() { + expectMissingJdbcUrlISE(); + + underTest.create(); + } + + @Test + public void create_from_scratch_creates_ServerId_from_JDBC_URL_and_new_uuid() { + String jdbcUrl = "jdbc"; + String uuid = Uuids.create(); + String sanitizedJdbcUrl = "sanitized_jdbc"; + settings.setProperty(JDBC_URL.getKey(), jdbcUrl); + when(uuidFactory.create()).thenReturn(uuid); + when(jdbcUrlSanitizer.sanitize(jdbcUrl)).thenReturn(sanitizedJdbcUrl); + + ServerId serverId = underTest.create(); + + assertThat(serverId.getDatabaseId().get()).isEqualTo(crc32Hex(sanitizedJdbcUrl)); + assertThat(serverId.getDatasetId()).isEqualTo(uuid); + } + + @Test + public void create_from_ServerId_fails_with_ISE_if_JDBC_property_not_set() { + expectMissingJdbcUrlISE(); + + underTest.create(A_SERVERID); + } + + @Test + @UseDataProvider("anyFormatServerId") + public void create_from_ServerId_creates_ServerId_from_JDBC_URL_and_serverId_datasetId(ServerId currentServerId) { + String jdbcUrl = "jdbc"; + String sanitizedJdbcUrl = "sanitized_jdbc"; + settings.setProperty(JDBC_URL.getKey(), jdbcUrl); + when(uuidFactory.create()).thenThrow(new IllegalStateException("UuidFactory.create() should not be called")); + when(jdbcUrlSanitizer.sanitize(jdbcUrl)).thenReturn(sanitizedJdbcUrl); + + ServerId serverId = underTest.create(currentServerId); + + assertThat(serverId.getDatabaseId().get()).isEqualTo(crc32Hex(sanitizedJdbcUrl)); + assertThat(serverId.getDatasetId()).isEqualTo(currentServerId.getDatasetId()); + } + + @DataProvider + public static Object[][] anyFormatServerId() { + return new Object[][] { + {ServerId.parse(new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()))}, + {ServerId.parse(randomAlphabetic(NOT_UUID_DATASET_ID_LENGTH))}, + {ServerId.parse(randomAlphabetic(UUID_DATASET_ID_LENGTH))}, + {ServerId.of(randomAlphabetic(DATABASE_ID_LENGTH), randomAlphabetic(NOT_UUID_DATASET_ID_LENGTH))}, + {ServerId.of(randomAlphabetic(DATABASE_ID_LENGTH), randomAlphabetic(UUID_DATASET_ID_LENGTH))} + }; + } + + private void expectMissingJdbcUrlISE() { + expectedException.expect(IllegalStateException.class); + expectedException.expectMessage("Missing JDBC URL"); + } +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/serverid/ServerIdManagerTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/serverid/ServerIdManagerTest.java new file mode 100644 index 00000000000..0875ad0fcb2 --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/serverid/ServerIdManagerTest.java @@ -0,0 +1,363 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.serverid; + +import com.tngtech.java.junit.dataprovider.DataProvider; +import com.tngtech.java.junit.dataprovider.DataProviderRunner; +import com.tngtech.java.junit.dataprovider.UseDataProvider; +import org.sonar.api.SonarEdition; +import org.junit.After; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.sonar.api.CoreProperties; +import org.sonar.api.SonarQubeSide; +import org.sonar.api.internal.SonarRuntimeImpl; +import org.sonar.api.utils.System2; +import org.sonar.api.utils.Version; +import org.sonar.core.platform.ServerId; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.DbTester; +import org.sonar.db.property.PropertyDto; +import org.sonar.server.platform.WebServer; +import org.sonar.server.property.InternalProperties; + +import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.sonar.api.SonarQubeSide.COMPUTE_ENGINE; +import static org.sonar.api.SonarQubeSide.SERVER; +import static org.sonar.core.platform.ServerId.DATABASE_ID_LENGTH; +import static org.sonar.core.platform.ServerId.NOT_UUID_DATASET_ID_LENGTH; +import static org.sonar.core.platform.ServerId.UUID_DATASET_ID_LENGTH; + +@RunWith(DataProviderRunner.class) +public class ServerIdManagerTest { + + private static final ServerId OLD_FORMAT_SERVER_ID = ServerId.parse("20161123150657"); + private static final ServerId NO_DATABASE_ID_SERVER_ID = ServerId.parse(randomAlphanumeric(UUID_DATASET_ID_LENGTH)); + private static final ServerId WITH_DATABASE_ID_SERVER_ID = ServerId.of(randomAlphanumeric(DATABASE_ID_LENGTH), randomAlphanumeric(NOT_UUID_DATASET_ID_LENGTH)); + private static final String CHECKSUM_1 = randomAlphanumeric(12); + + @Rule + public final DbTester dbTester = DbTester.create(System2.INSTANCE); + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + private ServerIdChecksum serverIdChecksum = mock(ServerIdChecksum.class); + private ServerIdFactory serverIdFactory = mock(ServerIdFactory.class); + private DbClient dbClient = dbTester.getDbClient(); + private DbSession dbSession = dbTester.getSession(); + private WebServer webServer = mock(WebServer.class); + private ServerIdManager underTest; + + @After + public void tearDown() { + if (underTest != null) { + underTest.stop(); + } + } + + @Test + public void web_leader_persists_new_server_id_if_missing() { + mockCreateNewServerId(WITH_DATABASE_ID_SERVER_ID); + mockChecksumOf(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1); + when(webServer.isStartupLeader()).thenReturn(true); + + test(SERVER); + + verifyDb(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1); + verifyCreateNewServerIdFromScratch(); + } + + @Test + public void web_leader_persists_new_server_id_if_format_is_old_date() { + insertServerId(OLD_FORMAT_SERVER_ID); + mockCreateNewServerId(WITH_DATABASE_ID_SERVER_ID); + mockChecksumOf(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1); + when(webServer.isStartupLeader()).thenReturn(true); + + test(SERVER); + + verifyDb(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1); + verifyCreateNewServerIdFromScratch(); + } + + @Test + public void web_leader_persists_new_server_id_if_value_is_empty() { + insertServerId(""); + mockCreateNewServerId(WITH_DATABASE_ID_SERVER_ID); + mockChecksumOf(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1); + when(webServer.isStartupLeader()).thenReturn(true); + + test(SERVER); + + verifyDb(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1); + verifyCreateNewServerIdFromScratch(); + } + + @Test + public void web_leader_keeps_existing_server_id_if_valid() { + insertServerId(WITH_DATABASE_ID_SERVER_ID); + insertChecksum(CHECKSUM_1); + mockChecksumOf(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1); + when(webServer.isStartupLeader()).thenReturn(true); + + test(SERVER); + + verifyDb(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1); + } + + @Test + public void web_leader_creates_server_id_from_scratch_if_checksum_fails_for_serverId_in_deprecated_format() { + ServerId currentServerId = OLD_FORMAT_SERVER_ID; + insertServerId(currentServerId); + insertChecksum("invalid"); + mockChecksumOf(currentServerId, "valid"); + mockCreateNewServerId(WITH_DATABASE_ID_SERVER_ID); + mockChecksumOf(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1); + when(webServer.isStartupLeader()).thenReturn(true); + + test(SERVER); + + verifyDb(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1); + verifyCreateNewServerIdFromScratch(); + } + + @Test + public void web_leader_creates_server_id_from_current_serverId_without_databaseId_if_checksum_fails() { + ServerId currentServerId = ServerId.parse(randomAlphanumeric(UUID_DATASET_ID_LENGTH)); + insertServerId(currentServerId); + insertChecksum("does_not_match_WITH_DATABASE_ID_SERVER_ID"); + mockChecksumOf(currentServerId, "matches_WITH_DATABASE_ID_SERVER_ID"); + mockCreateNewServerIdFrom(currentServerId, WITH_DATABASE_ID_SERVER_ID); + mockChecksumOf(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1); + when(webServer.isStartupLeader()).thenReturn(true); + + test(SERVER); + + verifyDb(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1); + verifyCreateNewServerIdFrom(currentServerId); + } + + @Test + public void web_leader_creates_server_id_from_current_serverId_with_databaseId_if_checksum_fails() { + ServerId currentServerId = ServerId.of(randomAlphanumeric(DATABASE_ID_LENGTH), randomAlphanumeric(UUID_DATASET_ID_LENGTH)); + insertServerId(currentServerId); + insertChecksum("does_not_match_WITH_DATABASE_ID_SERVER_ID"); + mockChecksumOf(currentServerId, "matches_WITH_DATABASE_ID_SERVER_ID"); + mockCreateNewServerIdFrom(currentServerId, WITH_DATABASE_ID_SERVER_ID); + mockChecksumOf(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1); + when(webServer.isStartupLeader()).thenReturn(true); + + test(SERVER); + + verifyDb(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1); + verifyCreateNewServerIdFrom(currentServerId); + } + + @Test + public void web_leader_generates_missing_checksum_for_current_serverId_with_databaseId() { + insertServerId(WITH_DATABASE_ID_SERVER_ID); + mockChecksumOf(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1); + when(webServer.isStartupLeader()).thenReturn(true); + + test(SERVER); + + verifyDb(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1); + } + + @Test + @UseDataProvider("allFormatsOfServerId") + public void web_follower_does_not_fail_if_server_id_matches_checksum(ServerId serverId) { + insertServerId(serverId); + insertChecksum(CHECKSUM_1); + mockChecksumOf(serverId, CHECKSUM_1); + when(webServer.isStartupLeader()).thenReturn(false); + + test(SERVER); + + // no changes + verifyDb(serverId, CHECKSUM_1); + } + + @Test + public void web_follower_fails_if_server_id_is_missing() { + when(webServer.isStartupLeader()).thenReturn(false); + + expectMissingServerIdException(); + + test(SERVER); + } + + @Test + public void web_follower_fails_if_server_id_is_empty() { + insertServerId(""); + when(webServer.isStartupLeader()).thenReturn(false); + + expectEmptyServerIdException(); + + test(SERVER); + } + + @Test + @UseDataProvider("allFormatsOfServerId") + public void web_follower_fails_if_checksum_does_not_match(ServerId serverId) { + String dbChecksum = "boom"; + insertServerId(serverId); + insertChecksum(dbChecksum); + mockChecksumOf(serverId, CHECKSUM_1); + when(webServer.isStartupLeader()).thenReturn(false); + + try { + test(SERVER); + fail("An ISE should have been raised"); + } + catch (IllegalStateException e) { + assertThat(e.getMessage()).isEqualTo("Server ID is invalid"); + // no changes + verifyDb(serverId, dbChecksum); + } + } + + @Test + @UseDataProvider("allFormatsOfServerId") + public void compute_engine_does_not_fail_if_server_id_is_valid(ServerId serverId) { + insertServerId(serverId); + insertChecksum(CHECKSUM_1); + mockChecksumOf(serverId, CHECKSUM_1); + + test(COMPUTE_ENGINE); + + // no changes + verifyDb(serverId, CHECKSUM_1); + } + + @Test + public void compute_engine_fails_if_server_id_is_missing() { + expectMissingServerIdException(); + + test(COMPUTE_ENGINE); + } + + @Test + public void compute_engine_fails_if_server_id_is_empty() { + insertServerId(""); + + expectEmptyServerIdException(); + + test(COMPUTE_ENGINE); + } + + @Test + @UseDataProvider("allFormatsOfServerId") + public void compute_engine_fails_if_server_id_is_invalid(ServerId serverId) { + String dbChecksum = "boom"; + insertServerId(serverId); + insertChecksum(dbChecksum); + mockChecksumOf(serverId, CHECKSUM_1); + + try { + test(SERVER); + fail("An ISE should have been raised"); + } + catch (IllegalStateException e) { + assertThat(e.getMessage()).isEqualTo("Server ID is invalid"); + // no changes + verifyDb(serverId, dbChecksum); + } + } + + @DataProvider + public static Object[][] allFormatsOfServerId() { + return new Object[][] { + {OLD_FORMAT_SERVER_ID}, + {NO_DATABASE_ID_SERVER_ID}, + {WITH_DATABASE_ID_SERVER_ID} + }; + } + + private void expectEmptyServerIdException() { + expectedException.expect(IllegalStateException.class); + expectedException.expectMessage("Property sonar.core.id is empty in database"); + } + + private void expectMissingServerIdException() { + expectedException.expect(IllegalStateException.class); + expectedException.expectMessage("Property sonar.core.id is missing in database"); + } + + private void verifyDb(ServerId expectedServerId, String expectedChecksum) { + assertThat(dbClient.propertiesDao().selectGlobalProperty(dbSession, CoreProperties.SERVER_ID)) + .extracting(PropertyDto::getValue) + .isEqualTo(expectedServerId.toString()); + assertThat(dbClient.internalPropertiesDao().selectByKey(dbSession, InternalProperties.SERVER_ID_CHECKSUM)) + .hasValue(expectedChecksum); + } + + private void mockCreateNewServerId(ServerId newServerId) { + when(serverIdFactory.create()).thenReturn(newServerId); + when(serverIdFactory.create(any())).thenThrow(new IllegalStateException("new ServerId should not be created from current server id")); + } + + private void mockCreateNewServerIdFrom(ServerId currentServerId, ServerId newServerId) { + when(serverIdFactory.create()).thenThrow(new IllegalStateException("new ServerId should be created from current server id")); + when(serverIdFactory.create(eq(currentServerId))).thenReturn(newServerId); + } + + private void verifyCreateNewServerIdFromScratch() { + verify(serverIdFactory).create(); + } + + private void verifyCreateNewServerIdFrom(ServerId currentServerId) { + verify(serverIdFactory).create(currentServerId); + } + + private void mockChecksumOf(ServerId serverId, String checksum1) { + when(serverIdChecksum.computeFor(serverId.toString())).thenReturn(checksum1); + } + + private void insertServerId(ServerId serverId) { + insertServerId(serverId.toString()); + } + + private void insertServerId(String serverId) { + dbClient.propertiesDao().saveProperty(dbSession, new PropertyDto().setKey(CoreProperties.SERVER_ID).setValue(serverId.toString())); + dbSession.commit(); + } + + private void insertChecksum(String value) { + dbClient.internalPropertiesDao().save(dbSession, InternalProperties.SERVER_ID_CHECKSUM, value); + dbSession.commit(); + } + + private void test(SonarQubeSide side) { + underTest = new ServerIdManager(serverIdChecksum, serverIdFactory, dbClient, SonarRuntimeImpl + .forSonarQube(Version.create(6, 7), side, SonarEdition.COMMUNITY), webServer); + underTest.start(); + } +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/serverid/ServerIdModuleTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/serverid/ServerIdModuleTest.java new file mode 100644 index 00000000000..0fd1a57bcec --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/serverid/ServerIdModuleTest.java @@ -0,0 +1,38 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.serverid; + +import org.junit.Test; +import org.sonar.core.platform.ComponentContainer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.core.platform.ComponentContainer.COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER; + +public class ServerIdModuleTest { + private ServerIdModule underTest = new ServerIdModule(); + + @Test + public void verify_count_of_added_components() { + ComponentContainer container = new ComponentContainer(); + underTest.configure(container); + assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 4); + } + +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/web/requestid/HttpRequestIdModuleTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/web/requestid/HttpRequestIdModuleTest.java new file mode 100644 index 00000000000..6d68d7502b9 --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/web/requestid/HttpRequestIdModuleTest.java @@ -0,0 +1,39 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.web.requestid; + +import org.junit.Test; +import org.sonar.core.platform.ComponentContainer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.core.platform.ComponentContainer.COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER; + +public class HttpRequestIdModuleTest { + private HttpRequestIdModule underTest = new HttpRequestIdModule(); + + @Test + public void count_components_in_module() { + ComponentContainer container = new ComponentContainer(); + underTest.configure(container); + + assertThat(container.getPicoContainer().getComponentAdapters()) + .hasSize(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 3); + } +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/web/requestid/RequestIdConfigurationTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/web/requestid/RequestIdConfigurationTest.java new file mode 100644 index 00000000000..84a62916785 --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/web/requestid/RequestIdConfigurationTest.java @@ -0,0 +1,33 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.web.requestid; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class RequestIdConfigurationTest { + private RequestIdConfiguration underTest = new RequestIdConfiguration(50); + + @Test + public void getUidGeneratorRenewalCount_returns_value_provided_from_constructor() { + assertThat(underTest.getUidGeneratorRenewalCount()).isEqualTo(50); + } +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/web/requestid/RequestIdGeneratorImplTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/web/requestid/RequestIdGeneratorImplTest.java new file mode 100644 index 00000000000..5e2bf3c2cf7 --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/web/requestid/RequestIdGeneratorImplTest.java @@ -0,0 +1,89 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.web.requestid; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.core.util.UuidGenerator; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class RequestIdGeneratorImplTest { + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + private UuidGenerator.WithFixedBase generator1 = increment -> new byte[] {124, 22, 66, 96, 55, 88, 2, 9}; + private UuidGenerator.WithFixedBase generator2 = increment -> new byte[] {0, 5, 88, 81, 8, 6, 44, 19}; + private UuidGenerator.WithFixedBase generator3 = increment -> new byte[] {126, 9, 35, 76, 2, 1, 2}; + private RequestIdGeneratorBase uidGeneratorBase = mock(RequestIdGeneratorBase.class); + private IllegalStateException expected = new IllegalStateException("Unexpected third call to createNew"); + + @Test + public void generate_renews_inner_UuidGenerator_instance_every_number_of_calls_to_generate_specified_in_RequestIdConfiguration_supports_2() { + when(uidGeneratorBase.createNew()) + .thenReturn(generator1) + .thenReturn(generator2) + .thenReturn(generator3) + .thenThrow(expected); + + RequestIdGeneratorImpl underTest = new RequestIdGeneratorImpl(uidGeneratorBase, new RequestIdConfiguration(2)); + + assertThat(underTest.generate()).isEqualTo("fBZCYDdYAgk="); // using generator1 + assertThat(underTest.generate()).isEqualTo("fBZCYDdYAgk="); // still using generator1 + assertThat(underTest.generate()).isEqualTo("AAVYUQgGLBM="); // renewing generator and using generator2 + assertThat(underTest.generate()).isEqualTo("AAVYUQgGLBM="); // still using generator2 + assertThat(underTest.generate()).isEqualTo("fgkjTAIBAg=="); // renewing generator and using generator3 + assertThat(underTest.generate()).isEqualTo("fgkjTAIBAg=="); // using generator3 + + expectedException.expect(IllegalStateException.class); + expectedException.expectMessage(expected.getMessage()); + + underTest.generate(); // renewing generator and failing + } + + @Test + public void generate_renews_inner_UuidGenerator_instance_every_number_of_calls_to_generate_specified_in_RequestIdConfiguration_supports_3() { + when(uidGeneratorBase.createNew()) + .thenReturn(generator1) + .thenReturn(generator2) + .thenReturn(generator3) + .thenThrow(expected); + + RequestIdGeneratorImpl underTest = new RequestIdGeneratorImpl(uidGeneratorBase, new RequestIdConfiguration(3)); + + assertThat(underTest.generate()).isEqualTo("fBZCYDdYAgk="); // using generator1 + assertThat(underTest.generate()).isEqualTo("fBZCYDdYAgk="); // still using generator1 + assertThat(underTest.generate()).isEqualTo("fBZCYDdYAgk="); // still using generator1 + assertThat(underTest.generate()).isEqualTo("AAVYUQgGLBM="); // renewing generator and using it + assertThat(underTest.generate()).isEqualTo("AAVYUQgGLBM="); // still using generator2 + assertThat(underTest.generate()).isEqualTo("AAVYUQgGLBM="); // still using generator2 + assertThat(underTest.generate()).isEqualTo("fgkjTAIBAg=="); // renewing generator and using it + assertThat(underTest.generate()).isEqualTo("fgkjTAIBAg=="); // using generator3 + assertThat(underTest.generate()).isEqualTo("fgkjTAIBAg=="); // using generator3 + + expectedException.expect(IllegalStateException.class); + expectedException.expectMessage(expected.getMessage()); + + underTest.generate(); // renewing generator and failing + } +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/CachingRuleFinderTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/CachingRuleFinderTest.java new file mode 100644 index 00000000000..6c19c1a0ef0 --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/CachingRuleFinderTest.java @@ -0,0 +1,424 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.rule; + +import com.google.common.collect.ImmutableSet; +import java.util.Arrays; +import java.util.List; +import java.util.Random; +import java.util.function.Consumer; +import org.junit.Before; +import org.junit.Test; +import org.sonar.api.impl.utils.AlwaysIncreasingSystem2; +import org.sonar.api.rule.RuleKey; +import org.sonar.api.rules.Rule; +import org.sonar.api.rules.RuleQuery; +import org.sonar.api.utils.System2; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.DbTester; +import org.sonar.db.rule.RuleDao; +import org.sonar.db.rule.RuleDefinitionDto; +import org.sonar.db.rule.RuleParamDto; +import org.sonar.db.rule.RuleTesting; + +import static java.util.stream.Collectors.toList; +import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +public class CachingRuleFinderTest { + @org.junit.Rule + public DbTester dbTester = DbTester.create(System2.INSTANCE); + + private DbClient dbClient = dbTester.getDbClient(); + + private AlwaysIncreasingSystem2 system2 = new AlwaysIncreasingSystem2(); + private RuleDefinitionDto[] ruleDefinitions; + private RuleParamDto[] ruleParams; + private CachingRuleFinder underTest; + + @Before() + public void setUp() throws Exception { + Consumer<RuleDefinitionDto> setUpdatedAt = rule -> rule.setUpdatedAt(system2.now()); + this.ruleDefinitions = new RuleDefinitionDto[] { + dbTester.rules().insert(setUpdatedAt), + dbTester.rules().insert(setUpdatedAt), + dbTester.rules().insert(setUpdatedAt), + dbTester.rules().insert(setUpdatedAt), + dbTester.rules().insert(setUpdatedAt), + dbTester.rules().insert(setUpdatedAt) + }; + this.ruleParams = Arrays.stream(ruleDefinitions) + .map(rule -> dbTester.rules().insertRuleParam(rule)) + .toArray(RuleParamDto[]::new); + + underTest = new CachingRuleFinder(dbClient); + + // delete all data from DB to ensure tests rely on cache exclusively + dbTester.executeUpdateSql("delete from rules"); + dbTester.executeUpdateSql("delete from rules_parameters"); + assertThat(dbTester.countRowsOfTable("rules")).isZero(); + assertThat(dbTester.countRowsOfTable("rules_parameters")).isZero(); + } + + @Test + public void constructor_reads_rules_from_DB() { + DbClient dbClient = mock(DbClient.class); + DbSession dbSession = mock(DbSession.class); + RuleDao ruleDao = mock(RuleDao.class); + when(dbClient.openSession(anyBoolean())).thenReturn(dbSession); + when(dbClient.ruleDao()).thenReturn(ruleDao); + + new CachingRuleFinder(dbClient); + + verify(dbClient).openSession(anyBoolean()); + verify(ruleDao).selectAllDefinitions(dbSession); + verifyNoMoreInteractions(ruleDao); + } + + @Test + public void constructor_reads_parameters_from_DB() { + DbClient dbClient = mock(DbClient.class); + DbSession dbSession = mock(DbSession.class); + RuleDao ruleDao = mock(RuleDao.class); + when(dbClient.openSession(anyBoolean())).thenReturn(dbSession); + when(dbClient.ruleDao()).thenReturn(ruleDao); + List<RuleKey> ruleKeys = Arrays.asList(RuleKey.of("A", "B"), RuleKey.of("C", "D"), RuleKey.of("E", "F")); + when(ruleDao.selectAllDefinitions(dbSession)).thenReturn(ruleKeys.stream().map(RuleTesting::newRule).collect(toList())); + + new CachingRuleFinder(dbClient); + + verify(ruleDao).selectRuleParamsByRuleKeys(dbSession, ImmutableSet.copyOf(ruleKeys)); + } + + @Test + public void findById_returns_all_loaded_rules_by_id() { + for (int i = 0; i < ruleDefinitions.length; i++) { + RuleDefinitionDto ruleDefinition = ruleDefinitions[i]; + RuleParamDto ruleParam = ruleParams[i]; + + org.sonar.api.rules.Rule rule = underTest.findById(ruleDefinition.getId()); + verifyRule(rule, ruleDefinition, ruleParam); + } + } + + @Test + public void findById_returns_null_for_non_existing_id() { + assertThat(underTest.findById(new Random().nextInt())).isNull(); + } + + @Test + public void findByKey_returns_all_loaded_rules_by_id() { + for (int i = 0; i < ruleDefinitions.length; i++) { + RuleDefinitionDto ruleDefinition = ruleDefinitions[i]; + RuleParamDto ruleParam = ruleParams[i]; + + org.sonar.api.rules.Rule rule = underTest.findByKey(ruleDefinition.getKey()); + verifyRule(rule, ruleDefinition, ruleParam); + assertThat(underTest.findByKey(ruleDefinition.getRepositoryKey(), ruleDefinition.getRuleKey())) + .isSameAs(rule); + } + } + + @Test + public void findByKey_returns_null_when_RuleKey_is_null() { + assertThat(underTest.findByKey(null)).isNull(); + } + + @Test + public void findByKey_returns_null_when_repository_key_is_null() { + assertThat(underTest.findByKey(null, randomAlphabetic(2))).isNull(); + } + + @Test + public void findByKey_returns_null_when_key_is_null() { + assertThat(underTest.findByKey(randomAlphabetic(2), null)).isNull(); + } + + @Test + public void findByKey_returns_null_when_both_repository_key_and_key_are_null() { + assertThat(underTest.findByKey(null, null)).isNull(); + } + + @Test + public void find_returns_null_when_RuleQuery_is_empty() { + assertThat(underTest.find(null)).isNull(); + } + + @Test + public void find_returns_most_recent_rule_when_RuleQuery_has_no_non_null_field() { + Rule rule = underTest.find(RuleQuery.create()); + + assertThat(toRuleKey(rule)).isEqualTo(ruleDefinitions[5].getKey()); + } + + @Test + public void find_searches_by_exact_match_of_repository_key_and_returns_most_recent_rule() { + String repoKey = "ABCD"; + RuleDefinitionDto[] sameRepoKey = { + dbTester.rules().insert(rule -> rule.setRepositoryKey(repoKey).setUpdatedAt(system2.now())), + dbTester.rules().insert(rule -> rule.setRepositoryKey(repoKey).setUpdatedAt(system2.now())) + }; + RuleDefinitionDto otherRule = dbTester.rules().insert(rule -> rule.setUpdatedAt(system2.now())); + + CachingRuleFinder underTest = new CachingRuleFinder(dbClient); + + assertThat(toRuleKey(underTest.find(RuleQuery.create().withRepositoryKey(repoKey)))) + .isEqualTo(sameRepoKey[1].getKey()); + assertThat(toRuleKey(underTest.find(RuleQuery.create().withRepositoryKey(otherRule.getRepositoryKey())))) + .isEqualTo(otherRule.getKey()); + assertThat(underTest.find(RuleQuery.create().withRepositoryKey(repoKey.toLowerCase()))) + .isNull(); + assertThat(underTest.find(RuleQuery.create().withRepositoryKey(randomAlphabetic(3)))) + .isNull(); + } + + @Test + public void find_searches_by_exact_match_of_ruleKey_and_returns_most_recent_rule() { + String ruleKey = "ABCD"; + RuleDefinitionDto[] sameRuleKey = { + dbTester.rules().insert(rule -> rule.setRuleKey(ruleKey).setUpdatedAt(system2.now())), + dbTester.rules().insert(rule -> rule.setRuleKey(ruleKey).setUpdatedAt(system2.now())) + }; + RuleDefinitionDto otherRule = dbTester.rules().insert(rule -> rule.setUpdatedAt(system2.now())); + + CachingRuleFinder underTest = new CachingRuleFinder(dbClient); + + assertThat(toRuleKey(underTest.find(RuleQuery.create().withKey(ruleKey)))) + .isEqualTo(sameRuleKey[1].getKey()); + assertThat(toRuleKey(underTest.find(RuleQuery.create().withKey(otherRule.getRuleKey())))) + .isEqualTo(otherRule.getKey()); + assertThat(underTest.find(RuleQuery.create().withKey(ruleKey.toLowerCase()))) + .isNull(); + assertThat(underTest.find(RuleQuery.create().withKey(randomAlphabetic(3)))) + .isNull(); + } + + @Test + public void find_searches_by_exact_match_of_configKey_and_returns_most_recent_rule() { + String configKey = "ABCD"; + RuleDefinitionDto[] sameConfigKey = { + dbTester.rules().insert(rule -> rule.setConfigKey(configKey).setUpdatedAt(system2.now())), + dbTester.rules().insert(rule -> rule.setConfigKey(configKey).setUpdatedAt(system2.now())) + }; + RuleDefinitionDto otherRule = dbTester.rules().insert(rule -> rule.setUpdatedAt(system2.now())); + + CachingRuleFinder underTest = new CachingRuleFinder(dbClient); + + assertThat(toRuleKey(underTest.find(RuleQuery.create().withConfigKey(configKey)))) + .isEqualTo(sameConfigKey[1].getKey()); + assertThat(toRuleKey(underTest.find(RuleQuery.create().withConfigKey(otherRule.getConfigKey())))) + .isEqualTo(otherRule.getKey()); + assertThat(underTest.find(RuleQuery.create().withConfigKey(configKey.toLowerCase()))) + .isNull(); + assertThat(underTest.find(RuleQuery.create().withConfigKey(randomAlphabetic(3)))) + .isNull(); + } + + @Test + public void find_searches_by_exact_match_and_match_on_all_criterias_and_returns_most_recent_match() { + String repoKey = "ABCD"; + String ruleKey = "EFGH"; + String configKey = "IJKL"; + RuleDefinitionDto[] rules = { + dbTester.rules().insert(rule -> rule.setRepositoryKey(repoKey).setRuleKey(ruleKey).setConfigKey(configKey).setUpdatedAt(system2.now())), + dbTester.rules().insert(rule -> rule.setRuleKey(ruleKey).setConfigKey(configKey).setUpdatedAt(system2.now())), + dbTester.rules().insert(rule -> rule.setRepositoryKey(repoKey).setConfigKey(configKey).setUpdatedAt(system2.now())), + dbTester.rules().insert(rule -> rule.setUpdatedAt(system2.now())) + }; + RuleQuery allQuery = RuleQuery.create().withRepositoryKey(repoKey).withKey(ruleKey).withConfigKey(configKey); + RuleQuery ruleAndConfigKeyQuery = RuleQuery.create().withKey(ruleKey).withConfigKey(configKey); + RuleQuery repoAndConfigKeyQuery = RuleQuery.create().withRepositoryKey(repoKey).withConfigKey(configKey); + RuleQuery repoAndKeyQuery = RuleQuery.create().withRepositoryKey(repoKey).withKey(ruleKey); + RuleQuery configKeyQuery = RuleQuery.create().withConfigKey(configKey); + RuleQuery ruleKeyQuery = RuleQuery.create().withKey(ruleKey); + RuleQuery repoKeyQuery = RuleQuery.create().withRepositoryKey(repoKey); + + CachingRuleFinder underTest = new CachingRuleFinder(dbClient); + + assertThat(toRuleKey(underTest.find(allQuery))).isEqualTo(rules[0].getKey()); + assertThat(toRuleKey(underTest.find(ruleAndConfigKeyQuery))).isEqualTo(rules[1].getKey()); + assertThat(toRuleKey(underTest.find(repoAndConfigKeyQuery))).isEqualTo(rules[2].getKey()); + assertThat(toRuleKey(underTest.find(repoAndKeyQuery))).isEqualTo(rules[0].getKey()); + assertThat(toRuleKey(underTest.find(repoKeyQuery))).isEqualTo(rules[2].getKey()); + assertThat(toRuleKey(underTest.find(ruleKeyQuery))).isEqualTo(rules[1].getKey()); + assertThat(toRuleKey(underTest.find(configKeyQuery))).isEqualTo(rules[2].getKey()); + } + + @Test + public void findAll_returns_empty_when_RuleQuery_is_empty() { + assertThat(underTest.findAll(null)).isEmpty(); + } + + @Test + public void findAll_returns_all_rules_when_RuleQuery_has_no_non_null_field() { + assertThat(underTest.findAll(RuleQuery.create())) + .extracting(CachingRuleFinderTest::toRuleKey) + .containsOnly(Arrays.stream(ruleDefinitions).map(RuleDefinitionDto::getKey).toArray(RuleKey[]::new)); + } + + @Test + public void findAll_returns_all_rules_with_exact_same_repository_key_and_order_them_most_recent_first() { + String repoKey = "ABCD"; + RuleDefinitionDto[] sameRepoKey = { + dbTester.rules().insert(rule -> rule.setRepositoryKey(repoKey).setUpdatedAt(system2.now())), + dbTester.rules().insert(rule -> rule.setRepositoryKey(repoKey).setUpdatedAt(system2.now())) + }; + RuleDefinitionDto otherRule = dbTester.rules().insert(rule -> rule.setUpdatedAt(system2.now())); + + CachingRuleFinder underTest = new CachingRuleFinder(dbClient); + + assertThat(underTest.findAll(RuleQuery.create().withRepositoryKey(repoKey))) + .extracting(CachingRuleFinderTest::toRuleKey) + .containsExactly(sameRepoKey[1].getKey(), sameRepoKey[0].getKey()); + assertThat(underTest.findAll(RuleQuery.create().withRepositoryKey(otherRule.getRepositoryKey()))) + .extracting(CachingRuleFinderTest::toRuleKey) + .containsExactly(otherRule.getKey()); + assertThat(underTest.findAll(RuleQuery.create().withRepositoryKey(repoKey.toLowerCase()))) + .isEmpty(); + assertThat(underTest.findAll(RuleQuery.create().withRepositoryKey(randomAlphabetic(3)))) + .isEmpty(); + } + + @Test + public void findAll_returns_all_rules_with_exact_same_rulekey_and_order_them_most_recent_first() { + String ruleKey = "ABCD"; + RuleDefinitionDto[] sameRuleKey = { + dbTester.rules().insert(rule -> rule.setRuleKey(ruleKey).setUpdatedAt(system2.now())), + dbTester.rules().insert(rule -> rule.setRuleKey(ruleKey).setUpdatedAt(system2.now())) + }; + RuleDefinitionDto otherRule = dbTester.rules().insert(rule -> rule.setUpdatedAt(system2.now())); + + CachingRuleFinder underTest = new CachingRuleFinder(dbClient); + + assertThat(underTest.findAll(RuleQuery.create().withKey(ruleKey))) + .extracting(CachingRuleFinderTest::toRuleKey) + .containsExactly(sameRuleKey[1].getKey(), sameRuleKey[0].getKey()); + assertThat(underTest.findAll(RuleQuery.create().withKey(otherRule.getRuleKey()))) + .extracting(CachingRuleFinderTest::toRuleKey) + .containsExactly(otherRule.getKey()); + assertThat(underTest.findAll(RuleQuery.create().withKey(ruleKey.toLowerCase()))) + .isEmpty(); + assertThat(underTest.findAll(RuleQuery.create().withKey(randomAlphabetic(3)))) + .isEmpty(); + } + + @Test + public void findAll_returns_all_rules_with_exact_same_configkey_and_order_them_most_recent_first() { + String configKey = "ABCD"; + RuleDefinitionDto[] sameConfigKey = { + dbTester.rules().insert(rule -> rule.setConfigKey(configKey).setUpdatedAt(system2.now())), + dbTester.rules().insert(rule -> rule.setConfigKey(configKey).setUpdatedAt(system2.now())) + }; + RuleDefinitionDto otherRule = dbTester.rules().insert(rule -> rule.setUpdatedAt(system2.now())); + + CachingRuleFinder underTest = new CachingRuleFinder(dbClient); + + assertThat(underTest.findAll(RuleQuery.create().withConfigKey(configKey))) + .extracting(CachingRuleFinderTest::toRuleKey) + .containsExactly(sameConfigKey[1].getKey(), sameConfigKey[0].getKey()); + assertThat(underTest.findAll(RuleQuery.create().withConfigKey(otherRule.getConfigKey()))) + .extracting(CachingRuleFinderTest::toRuleKey) + .containsExactly(otherRule.getKey()); + assertThat(underTest.findAll(RuleQuery.create().withConfigKey(configKey.toLowerCase()))) + .isEmpty(); + assertThat(underTest.findAll(RuleQuery.create().withConfigKey(randomAlphabetic(3)))) + .isEmpty(); + } + + @Test + public void findAll_returns_all_rules_which_match_exactly_all_criteria_and_order_then_by_most_recent_first() { + String repoKey = "ABCD"; + String ruleKey = "EFGH"; + String configKey = "IJKL"; + RuleDefinitionDto[] rules = { + dbTester.rules().insert(rule -> rule.setRepositoryKey(repoKey).setRuleKey(ruleKey).setConfigKey(configKey).setUpdatedAt(system2.now())), + dbTester.rules().insert(rule -> rule.setRuleKey(ruleKey).setConfigKey(configKey).setUpdatedAt(system2.now())), + dbTester.rules().insert(rule -> rule.setRepositoryKey(repoKey).setConfigKey(configKey).setUpdatedAt(system2.now())), + dbTester.rules().insert(rule -> rule.setUpdatedAt(system2.now())) + }; + RuleQuery allQuery = RuleQuery.create().withRepositoryKey(repoKey).withKey(ruleKey).withConfigKey(configKey); + RuleQuery ruleAndConfigKeyQuery = RuleQuery.create().withKey(ruleKey).withConfigKey(configKey); + RuleQuery repoAndConfigKeyQuery = RuleQuery.create().withRepositoryKey(repoKey).withConfigKey(configKey); + RuleQuery repoAndKeyQuery = RuleQuery.create().withRepositoryKey(repoKey).withKey(ruleKey); + RuleQuery configKeyQuery = RuleQuery.create().withConfigKey(configKey); + RuleQuery ruleKeyQuery = RuleQuery.create().withKey(ruleKey); + RuleQuery repoKeyQuery = RuleQuery.create().withRepositoryKey(repoKey); + + CachingRuleFinder underTest = new CachingRuleFinder(dbClient); + + assertThat(underTest.findAll(allQuery)) + .extracting(CachingRuleFinderTest::toRuleKey) + .containsExactly(rules[0].getKey()); + assertThat(underTest.findAll(ruleAndConfigKeyQuery)) + .extracting(CachingRuleFinderTest::toRuleKey) + .containsExactly(rules[1].getKey(), rules[0].getKey()); + assertThat(underTest.findAll(repoAndConfigKeyQuery)) + .extracting(CachingRuleFinderTest::toRuleKey) + .containsExactly(rules[2].getKey(), rules[0].getKey()); + assertThat(underTest.findAll(repoAndKeyQuery)) + .extracting(CachingRuleFinderTest::toRuleKey) + .containsExactly(rules[0].getKey()); + assertThat(underTest.findAll(repoKeyQuery)) + .extracting(CachingRuleFinderTest::toRuleKey) + .containsExactly(rules[2].getKey(), rules[0].getKey()); + assertThat(underTest.findAll(ruleKeyQuery)) + .extracting(CachingRuleFinderTest::toRuleKey) + .containsExactly(rules[1].getKey(), rules[0].getKey()); + assertThat(underTest.findAll(configKeyQuery)) + .extracting(CachingRuleFinderTest::toRuleKey) + .containsExactly(rules[2].getKey(), rules[1].getKey(), rules[0].getKey()); + } + + private static RuleKey toRuleKey(Rule rule) { + return RuleKey.of(rule.getRepositoryKey(), rule.getKey()); + } + + private void verifyRule(Rule rule, RuleDefinitionDto ruleDefinition, RuleParamDto ruleParam) { + assertThat(rule).isNotNull(); + + assertThat(rule.getName()).isEqualTo(ruleDefinition.getName()); + assertThat(rule.getLanguage()).isEqualTo(ruleDefinition.getLanguage()); + assertThat(rule.getKey()).isEqualTo(ruleDefinition.getRuleKey()); + assertThat(rule.getConfigKey()).isEqualTo(ruleDefinition.getConfigKey()); + assertThat(rule.isTemplate()).isEqualTo(ruleDefinition.isTemplate()); + assertThat(rule.getCreatedAt().getTime()).isEqualTo(ruleDefinition.getCreatedAt()); + assertThat(rule.getUpdatedAt().getTime()).isEqualTo(ruleDefinition.getUpdatedAt()); + assertThat(rule.getRepositoryKey()).isEqualTo(ruleDefinition.getRepositoryKey()); + assertThat(rule.getSeverity().name()).isEqualTo(ruleDefinition.getSeverityString()); + assertThat(rule.getSystemTags()).isEqualTo(ruleDefinition.getSystemTags().stream().toArray(String[]::new)); + assertThat(rule.getTags()).isEmpty(); + assertThat(rule.getId()).isEqualTo(ruleDefinition.getId()); + assertThat(rule.getDescription()).isEqualTo(ruleDefinition.getDescription()); + + assertThat(rule.getParams()).hasSize(1); + org.sonar.api.rules.RuleParam param = rule.getParams().iterator().next(); + assertThat(param.getRule()).isSameAs(rule); + assertThat(param.getKey()).isEqualTo(ruleParam.getName()); + assertThat(param.getDescription()).isEqualTo(ruleParam.getDescription()); + assertThat(param.getType()).isEqualTo(ruleParam.getType()); + assertThat(param.getDefaultValue()).isEqualTo(ruleParam.getDefaultValue()); + } +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/DeprecatedRulesDefinitionLoaderTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/DeprecatedRulesDefinitionLoaderTest.java new file mode 100644 index 00000000000..ca338616a5d --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/DeprecatedRulesDefinitionLoaderTest.java @@ -0,0 +1,219 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.rule; + +import java.io.Reader; +import java.util.Arrays; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.sonar.api.rule.RuleKey; +import org.sonar.api.rule.RuleStatus; +import org.sonar.api.rule.Severity; +import org.sonar.api.rules.Rule; +import org.sonar.api.rules.RulePriority; +import org.sonar.api.rules.RuleRepository; +import org.sonar.api.server.debt.DebtRemediationFunction; +import org.sonar.api.server.rule.RulesDefinition; +import org.sonar.api.utils.ValidationMessages; +import org.sonar.core.i18n.RuleI18nManager; +import org.sonar.api.impl.server.RulesDefinitionContext; +import org.sonar.server.debt.DebtModelPluginRepository; +import org.sonar.server.debt.DebtModelXMLExporter; +import org.sonar.server.debt.DebtRulesXMLImporter; +import org.sonar.server.plugins.ServerPluginRepository; + +import static com.google.common.collect.Lists.newArrayList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class DeprecatedRulesDefinitionLoaderTest { + + @Mock + RuleI18nManager i18n; + + @Mock + DebtModelPluginRepository debtModelRepository; + + @Mock + DebtRulesXMLImporter importer; + + @Mock + ServerPluginRepository pluginRepository; + + static class CheckstyleRules extends RuleRepository { + public CheckstyleRules() { + super("checkstyle", "java"); + setName("Checkstyle"); + } + + @Override + public List<Rule> createRules() { + Rule rule = Rule.create("checkstyle", "ConstantName", "Constant Name"); + rule.setDescription("Checks that constant names conform to the specified format"); + rule.setConfigKey("Checker/TreeWalker/ConstantName"); + rule.setSeverity(RulePriority.BLOCKER); + rule.setStatus(Rule.STATUS_BETA); + rule.setTags(new String[] {"style", "clumsy"}); + rule.createParameter("format").setDescription("Regular expression").setDefaultValue("A-Z").setType("REGULAR_EXPRESSION"); + return Arrays.asList(rule); + } + } + + static class UseBundles extends RuleRepository { + public UseBundles() { + super("checkstyle", "java"); + setName("Checkstyle"); + } + + @Override + public List<Rule> createRules() { + Rule rule = Rule.create("checkstyle", "ConstantName"); + rule.createParameter("format"); + return Arrays.asList(rule); + } + } + + @Test + public void wrap_deprecated_rule_repositories() { + RulesDefinition.Context context = new RulesDefinitionContext(); + CheckstyleRules checkstyleRules = new CheckstyleRules(); + when(pluginRepository.getPluginKey(checkstyleRules)).thenReturn("unittest"); + new DeprecatedRulesDefinitionLoader(i18n, debtModelRepository, importer, pluginRepository, new RuleRepository[] {checkstyleRules}).complete(context); + + assertThat(context.repositories()).hasSize(1); + RulesDefinition.Repository checkstyle = context.repository("checkstyle"); + assertThat(checkstyle).isNotNull(); + assertThat(checkstyle.key()).isEqualTo("checkstyle"); + assertThat(checkstyle.name()).isEqualTo("Checkstyle"); + assertThat(checkstyle.language()).isEqualTo("java"); + assertThat(checkstyle.rules()).hasSize(1); + RulesDefinition.Rule rule = checkstyle.rule("ConstantName"); + assertThat(rule).isNotNull(); + assertThat(rule.key()).isEqualTo("ConstantName"); + assertThat(rule.pluginKey()).isEqualTo("unittest"); + assertThat(rule.name()).isEqualTo("Constant Name"); + assertThat(rule.htmlDescription()).isEqualTo("Checks that constant names conform to the specified format"); + assertThat(rule.severity()).isEqualTo(Severity.BLOCKER); + assertThat(rule.internalKey()).isEqualTo("Checker/TreeWalker/ConstantName"); + assertThat(rule.status()).isEqualTo(RuleStatus.BETA); + assertThat(rule.tags()).containsOnly("style", "clumsy"); + assertThat(rule.params()).hasSize(1); + RulesDefinition.Param param = rule.param("format"); + assertThat(param).isNotNull(); + assertThat(param.key()).isEqualTo("format"); + assertThat(param.name()).isEqualTo("format"); + assertThat(param.description()).isEqualTo("Regular expression"); + assertThat(param.defaultValue()).isEqualTo("A-Z"); + } + + @Test + public void emulate_the_day_deprecated_api_can_be_dropped() { + RulesDefinition.Context context = new RulesDefinitionContext(); + + // no more RuleRepository ! + new DeprecatedRulesDefinitionLoader(i18n, debtModelRepository, importer, pluginRepository); + + assertThat(context.repositories()).isEmpty(); + } + + @Test + public void use_l10n_bundles() { + RulesDefinition.Context context = new RulesDefinitionContext(); + when(i18n.getName("checkstyle", "ConstantName")).thenReturn("Constant Name"); + when(i18n.getDescription("checkstyle", "ConstantName")).thenReturn("Checks that constant names conform to the specified format"); + when(i18n.getParamDescription("checkstyle", "ConstantName", "format")).thenReturn("Regular expression"); + + new DeprecatedRulesDefinitionLoader(i18n, debtModelRepository, importer, pluginRepository, new RuleRepository[] {new UseBundles()}).complete(context); + + RulesDefinition.Repository checkstyle = context.repository("checkstyle"); + RulesDefinition.Rule rule = checkstyle.rule("ConstantName"); + assertThat(rule.key()).isEqualTo("ConstantName"); + assertThat(rule.name()).isEqualTo("Constant Name"); + assertThat(rule.htmlDescription()).isEqualTo("Checks that constant names conform to the specified format"); + RulesDefinition.Param param = rule.param("format"); + assertThat(param.key()).isEqualTo("format"); + assertThat(param.name()).isEqualTo("format"); + assertThat(param.description()).isEqualTo("Regular expression"); + } + + @Test + public void define_rule_debt() { + RulesDefinition.Context context = new RulesDefinitionContext(); + + List<DebtModelXMLExporter.RuleDebt> ruleDebts = newArrayList( + new DebtModelXMLExporter.RuleDebt() + .setRuleKey(RuleKey.of("checkstyle", "ConstantName")) + .setFunction(DebtRemediationFunction.Type.LINEAR_OFFSET.name()) + .setCoefficient("1d") + .setOffset("10min")); + + Reader javaModelReader = mock(Reader.class); + when(debtModelRepository.createReaderForXMLFile("java")).thenReturn(javaModelReader); + when(debtModelRepository.getContributingPluginList()).thenReturn(newArrayList("java")); + when(importer.importXML(eq(javaModelReader), any(ValidationMessages.class))).thenReturn(ruleDebts); + + new DeprecatedRulesDefinitionLoader(i18n, debtModelRepository, importer, pluginRepository, new RuleRepository[] {new CheckstyleRules()}).complete(context); + + assertThat(context.repositories()).hasSize(1); + RulesDefinition.Repository checkstyle = context.repository("checkstyle"); + assertThat(checkstyle.rules()).hasSize(1); + + RulesDefinition.Rule rule = checkstyle.rule("ConstantName"); + assertThat(rule).isNotNull(); + assertThat(rule.key()).isEqualTo("ConstantName"); + assertThat(rule.debtRemediationFunction().type()).isEqualTo(DebtRemediationFunction.Type.LINEAR_OFFSET); + assertThat(rule.debtRemediationFunction().gapMultiplier()).isEqualTo("1d"); + assertThat(rule.debtRemediationFunction().baseEffort()).isEqualTo("10min"); + } + + @Test + public void fail_on_invalid_rule_debt() { + RulesDefinition.Context context = new RulesDefinitionContext(); + + List<DebtModelXMLExporter.RuleDebt> ruleDebts = newArrayList( + new DebtModelXMLExporter.RuleDebt() + .setRuleKey(RuleKey.of("checkstyle", "ConstantName")) + .setFunction(DebtRemediationFunction.Type.LINEAR_OFFSET.name()) + .setCoefficient("1d")); + + Reader javaModelReader = mock(Reader.class); + when(debtModelRepository.createReaderForXMLFile("java")).thenReturn(javaModelReader); + when(debtModelRepository.getContributingPluginList()).thenReturn(newArrayList("java")); + when(importer.importXML(eq(javaModelReader), any(ValidationMessages.class))).thenReturn(ruleDebts); + + try { + new DeprecatedRulesDefinitionLoader(i18n, debtModelRepository, importer, pluginRepository, new RuleRepository[] {new CheckstyleRules()}).complete(context); + fail(); + } catch (Exception e) { + assertThat(e).isInstanceOf(IllegalArgumentException.class); + } + + assertThat(context.repositories()).isEmpty(); + } + +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/RuleDefinitionsLoaderTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/RuleDefinitionsLoaderTest.java new file mode 100644 index 00000000000..3cc43fa62d3 --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/RuleDefinitionsLoaderTest.java @@ -0,0 +1,126 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.rule; + +import org.junit.Test; +import org.sonar.api.server.rule.RulesDefinition; +import org.sonar.server.plugins.ServerPluginRepository; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +public class RuleDefinitionsLoaderTest { + + @Test + public void no_definitions() { + CommonRuleDefinitions commonRulesDefinitions = mock(CommonRuleDefinitions.class); + RulesDefinition.Context context = new RuleDefinitionsLoader(mock(DeprecatedRulesDefinitionLoader.class), commonRulesDefinitions, mock(ServerPluginRepository.class)).load(); + + assertThat(context.repositories()).isEmpty(); + } + + @Test + public void load_definitions() { + CommonRuleDefinitions commonRulesDefinitions = mock(CommonRuleDefinitions.class); + RulesDefinition.Context context = new RuleDefinitionsLoader(mock(DeprecatedRulesDefinitionLoader.class), commonRulesDefinitions, mock(ServerPluginRepository.class), + new RulesDefinition[] { + new FindbugsDefinitions(), new SquidDefinitions() + }).load(); + + assertThat(context.repositories()).hasSize(2); + assertThat(context.repository("findbugs")).isNotNull(); + assertThat(context.repository("squid")).isNotNull(); + } + + @Test + public void define_common_rules() { + CommonRuleDefinitions commonRulesDefinitions = new FakeCommonRuleDefinitions(); + RulesDefinition.Context context = new RuleDefinitionsLoader(mock(DeprecatedRulesDefinitionLoader.class), commonRulesDefinitions, mock(ServerPluginRepository.class), + new RulesDefinition[] { + new SquidDefinitions() + }).load(); + + assertThat(context.repositories()).extracting("key").containsOnly("squid", "common-java"); + assertThat(context.repository("common-java").rules()).extracting("key").containsOnly("InsufficientBranchCoverage"); + } + + /** + * "common-rules" are merged into core 5.2. Previously they were embedded by some plugins. Only the core definition + * is taken into account. Others are ignored. + */ + @Test + public void plugin_common_rules_are_overridden() { + CommonRuleDefinitions commonRulesDefinitions = new FakeCommonRuleDefinitions(); + RulesDefinition.Context context = new RuleDefinitionsLoader(mock(DeprecatedRulesDefinitionLoader.class), commonRulesDefinitions, mock(ServerPluginRepository.class), + new RulesDefinition[] { + new PluginCommonRuleDefinitions() + }).load(); + + assertThat(context.repositories()).extracting("key").containsOnly("common-java"); + assertThat(context.repository("common-java").rules()).extracting("key").containsOnly("InsufficientBranchCoverage"); + assertThat(context.repository("common-java").rule("InsufficientBranchCoverage").name()).isEqualTo("The name as defined by core"); + } + + static class FindbugsDefinitions implements RulesDefinition { + @Override + public void define(Context context) { + NewRepository repo = context.createRepository("findbugs", "java"); + repo.setName("Findbugs"); + repo.createRule("ABC") + .setName("ABC") + .setHtmlDescription("Description of ABC"); + repo.done(); + } + } + + static class SquidDefinitions implements RulesDefinition { + @Override + public void define(Context context) { + NewRepository repo = context.createRepository("squid", "java"); + repo.setName("Squid"); + repo.createRule("DEF") + .setName("DEF") + .setHtmlDescription("Description of DEF"); + repo.done(); + } + } + + static class PluginCommonRuleDefinitions implements RulesDefinition { + @Override + public void define(RulesDefinition.Context context) { + RulesDefinition.NewRepository repo = context.createRepository("common-java", "java"); + repo.createRule("InsufficientBranchCoverage") + .setName("The name as defined by plugin") + .setHtmlDescription("The description as defined by plugin"); + repo.done(); + } + } + + static class FakeCommonRuleDefinitions implements CommonRuleDefinitions { + @Override + public void define(RulesDefinition.Context context) { + RulesDefinition.NewRepository repo = context.createRepository("common-java", "java"); + repo.createRule("InsufficientBranchCoverage") + .setName("The name as defined by core") + .setHtmlDescription("The description as defined by core"); + repo.done(); + } + } +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/SingleDeprecatedRuleKeyTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/SingleDeprecatedRuleKeyTest.java new file mode 100644 index 00000000000..4b4b286a962 --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/SingleDeprecatedRuleKeyTest.java @@ -0,0 +1,128 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.rule; + +import com.google.common.collect.ImmutableSet; +import java.util.Set; +import org.assertj.core.groups.Tuple; +import org.junit.Test; +import org.sonar.api.rule.RuleKey; +import org.sonar.api.server.rule.RulesDefinition; +import org.sonar.core.util.stream.MoreCollectors; +import org.sonar.db.rule.DeprecatedRuleKeyDto; + +import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric; +import static org.apache.commons.lang.math.RandomUtils.nextInt; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.groups.Tuple.tuple; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class SingleDeprecatedRuleKeyTest { + + @Test + public void test_creation_from_DeprecatedRuleKeyDto() { + // Creation from DeprecatedRuleKeyDto + DeprecatedRuleKeyDto deprecatedRuleKeyDto = new DeprecatedRuleKeyDto() + .setOldRuleKey(randomAlphanumeric(50)) + .setOldRepositoryKey(randomAlphanumeric(50)) + .setRuleId(nextInt(1000)) + .setUuid(randomAlphanumeric(40)); + + SingleDeprecatedRuleKey singleDeprecatedRuleKey = SingleDeprecatedRuleKey.from(deprecatedRuleKeyDto); + + assertThat(singleDeprecatedRuleKey.getOldRepositoryKey()).isEqualTo(deprecatedRuleKeyDto.getOldRepositoryKey()); + assertThat(singleDeprecatedRuleKey.getOldRuleKey()).isEqualTo(deprecatedRuleKeyDto.getOldRuleKey()); + assertThat(singleDeprecatedRuleKey.getNewRepositoryKey()).isEqualTo(deprecatedRuleKeyDto.getNewRepositoryKey()); + assertThat(singleDeprecatedRuleKey.getNewRuleKey()).isEqualTo(deprecatedRuleKeyDto.getNewRuleKey()); + assertThat(singleDeprecatedRuleKey.getUuid()).isEqualTo(deprecatedRuleKeyDto.getUuid()); + assertThat(singleDeprecatedRuleKey.getRuleId()).isEqualTo(deprecatedRuleKeyDto.getRuleId()); + assertThat(singleDeprecatedRuleKey.getOldRuleKeyAsRuleKey()) + .isEqualTo(RuleKey.of(deprecatedRuleKeyDto.getOldRepositoryKey(), deprecatedRuleKeyDto.getOldRuleKey())); + } + + @Test + public void test_creation_from_RulesDefinitionRule() { + // Creation from RulesDefinition.Rule + ImmutableSet<RuleKey> deprecatedRuleKeys = ImmutableSet.of( + RuleKey.of(randomAlphanumeric(50), randomAlphanumeric(50)), + RuleKey.of(randomAlphanumeric(50), randomAlphanumeric(50)), + RuleKey.of(randomAlphanumeric(50), randomAlphanumeric(50)) + ); + + RulesDefinition.Repository repository = mock(RulesDefinition.Repository.class); + when(repository.key()).thenReturn(randomAlphanumeric(50)); + + RulesDefinition.Rule rule = mock(RulesDefinition.Rule.class); + when(rule.key()).thenReturn(randomAlphanumeric(50)); + when(rule.deprecatedRuleKeys()).thenReturn(deprecatedRuleKeys); + when(rule.repository()).thenReturn(repository); + + Set<SingleDeprecatedRuleKey> singleDeprecatedRuleKeys = SingleDeprecatedRuleKey.from(rule); + assertThat(singleDeprecatedRuleKeys).hasSize(deprecatedRuleKeys.size()); + assertThat(singleDeprecatedRuleKeys) + .extracting(SingleDeprecatedRuleKey::getUuid, SingleDeprecatedRuleKey::getOldRepositoryKey, SingleDeprecatedRuleKey::getOldRuleKey, + SingleDeprecatedRuleKey::getNewRepositoryKey, SingleDeprecatedRuleKey::getNewRuleKey, SingleDeprecatedRuleKey::getOldRuleKeyAsRuleKey) + .containsExactlyInAnyOrder( + deprecatedRuleKeys.stream().map( + r -> tuple(null, r.repository(), r.rule(), rule.repository().key(), rule.key(), RuleKey.of(r.repository(), r.rule())) + ).collect(MoreCollectors.toArrayList(deprecatedRuleKeys.size())).toArray(new Tuple[deprecatedRuleKeys.size()]) + ); + } + + @Test + public void test_equality() { + DeprecatedRuleKeyDto deprecatedRuleKeyDto1 = new DeprecatedRuleKeyDto() + .setOldRuleKey(randomAlphanumeric(50)) + .setOldRepositoryKey(randomAlphanumeric(50)) + .setUuid(randomAlphanumeric(40)) + .setRuleId(1); + + DeprecatedRuleKeyDto deprecatedRuleKeyDto1WithoutUuid = new DeprecatedRuleKeyDto() + .setOldRuleKey(deprecatedRuleKeyDto1.getOldRuleKey()) + .setOldRepositoryKey(deprecatedRuleKeyDto1.getOldRepositoryKey()); + + DeprecatedRuleKeyDto deprecatedRuleKeyDto2 = new DeprecatedRuleKeyDto() + .setOldRuleKey(randomAlphanumeric(50)) + .setOldRepositoryKey(randomAlphanumeric(50)) + .setUuid(randomAlphanumeric(40)); + + SingleDeprecatedRuleKey singleDeprecatedRuleKey1 = SingleDeprecatedRuleKey.from(deprecatedRuleKeyDto1); + SingleDeprecatedRuleKey singleDeprecatedRuleKey2 = SingleDeprecatedRuleKey.from(deprecatedRuleKeyDto2); + + assertThat(singleDeprecatedRuleKey1).isEqualTo(singleDeprecatedRuleKey1); + assertThat(singleDeprecatedRuleKey1).isEqualTo(SingleDeprecatedRuleKey.from(deprecatedRuleKeyDto1)); + assertThat(singleDeprecatedRuleKey1).isEqualTo(SingleDeprecatedRuleKey.from(deprecatedRuleKeyDto1WithoutUuid)); + assertThat(singleDeprecatedRuleKey2).isEqualTo(SingleDeprecatedRuleKey.from(deprecatedRuleKeyDto2)); + + assertThat(singleDeprecatedRuleKey1.hashCode()).isEqualTo(singleDeprecatedRuleKey1.hashCode()); + assertThat(singleDeprecatedRuleKey1.hashCode()).isEqualTo(SingleDeprecatedRuleKey.from(deprecatedRuleKeyDto1).hashCode()); + assertThat(singleDeprecatedRuleKey1.hashCode()).isEqualTo(SingleDeprecatedRuleKey.from(deprecatedRuleKeyDto1WithoutUuid).hashCode()); + assertThat(singleDeprecatedRuleKey2.hashCode()).isEqualTo(SingleDeprecatedRuleKey.from(deprecatedRuleKeyDto2).hashCode()); + + assertThat(singleDeprecatedRuleKey1).isNotEqualTo(null); + assertThat(singleDeprecatedRuleKey1).isNotEqualTo(""); + assertThat(singleDeprecatedRuleKey1).isNotEqualTo(singleDeprecatedRuleKey2); + assertThat(singleDeprecatedRuleKey2).isNotEqualTo(singleDeprecatedRuleKey1); + + assertThat(singleDeprecatedRuleKey1.hashCode()).isNotEqualTo(singleDeprecatedRuleKey2.hashCode()); + assertThat(singleDeprecatedRuleKey2.hashCode()).isNotEqualTo(singleDeprecatedRuleKey1.hashCode()); + } +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/WebServerRuleFinderImplTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/WebServerRuleFinderImplTest.java new file mode 100644 index 00000000000..5fdac77d9e8 --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/WebServerRuleFinderImplTest.java @@ -0,0 +1,65 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.rule; + +import org.junit.Before; +import org.junit.Test; +import org.sonar.api.rules.RuleFinder; +import org.sonar.db.DbClient; +import org.sonar.db.rule.RuleDao; +import org.sonar.server.organization.TestDefaultOrganizationProvider; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class WebServerRuleFinderImplTest { + + private DbClient dbClient = mock(DbClient.class); + private TestDefaultOrganizationProvider defaultOrganizationProvider = TestDefaultOrganizationProvider.fromUuid("1111"); + private WebServerRuleFinderImpl underTest = new WebServerRuleFinderImpl(dbClient, defaultOrganizationProvider); + + @Before + public void setUp() throws Exception { + when(dbClient.ruleDao()).thenReturn(mock(RuleDao.class)); + } + + @Test + public void constructor_initializes_with_non_caching_delegate() { + assertThat(underTest.delegate).isInstanceOf(DefaultRuleFinder.class); + } + + @Test + public void startCaching_sets_caching_delegate() { + underTest.startCaching(); + + assertThat(underTest.delegate).isInstanceOf(CachingRuleFinder.class); + } + + @Test + public void stopCaching_restores_non_caching_delegate() { + RuleFinder nonCachingDelegate = underTest.delegate; + + underTest.startCaching(); + underTest.stopCaching(); + + assertThat(underTest.delegate).isSameAs(nonCachingDelegate); + } +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/search/BaseDocTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/search/BaseDocTest.java new file mode 100644 index 00000000000..450bf1b96d2 --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/search/BaseDocTest.java @@ -0,0 +1,159 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.search; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import java.util.Collections; +import java.util.Date; +import java.util.Map; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.server.es.BaseDoc; +import org.sonar.server.es.EsUtils; +import org.sonar.server.es.Index; +import org.sonar.server.es.IndexType; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.fail; + +public class BaseDocTest { + private final IndexType.IndexMainType someType = IndexType.main(Index.simple("bar"), "donut"); + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Test + public void getField() { + Map<String, Object> fields = Maps.newHashMap(); + fields.put("a_string", "foo"); + fields.put("a_int", 42); + fields.put("a_null", null); + BaseDoc doc = new BaseDoc(someType, fields) { + @Override + public String getId() { + return null; + } + + }; + + assertThat((String) doc.getNullableField("a_string")).isEqualTo("foo"); + assertThat((int) doc.getNullableField("a_int")).isEqualTo(42); + assertThat((String) doc.getNullableField("a_null")).isNull(); + } + + @Test + public void getField_fails_if_missing_field() { + Map<String, Object> fields = Collections.emptyMap(); + BaseDoc doc = new BaseDoc(someType, fields) { + @Override + public String getId() { + return null; + } + + }; + + try { + doc.getNullableField("a_string"); + fail(); + } catch (IllegalStateException e) { + assertThat(e).hasMessage("Field a_string not specified in query options"); + } + } + + @Test + public void getFieldAsDate() { + BaseDoc doc = new BaseDoc(someType, Maps.newHashMap()) { + @Override + public String getId() { + return null; + } + + }; + Date now = new Date(); + doc.setField("javaDate", now); + assertThat(doc.getFieldAsDate("javaDate")).isEqualToIgnoringMillis(now); + + doc.setField("stringDate", EsUtils.formatDateTime(now)); + assertThat(doc.getFieldAsDate("stringDate")).isEqualToIgnoringMillis(now); + } + + @Test + public void getNullableFieldAsDate() { + BaseDoc doc = new BaseDoc(someType, Maps.newHashMap()) { + @Override + public String getId() { + return null; + } + + }; + Date now = new Date(); + doc.setField("javaDate", now); + assertThat(doc.getNullableFieldAsDate("javaDate")).isEqualToIgnoringMillis(now); + + doc.setField("stringDate", EsUtils.formatDateTime(now)); + assertThat(doc.getNullableFieldAsDate("stringDate")).isEqualToIgnoringMillis(now); + + doc.setField("noValue", null); + assertThat(doc.getNullableFieldAsDate("noValue")).isNull(); + } + + @Test + public void getFields_fails_with_ISE_if_setParent_has_not_been_called_on_IndexRelationType() { + IndexType.IndexRelationType relationType = IndexType.relation(IndexType.main(Index.withRelations("foo"), "bar"), "donut"); + BaseDoc doc = new BaseDoc(relationType) { + + @Override + public String getId() { + throw new UnsupportedOperationException("getId not implemented"); + } + + }; + + expectedException.expect(IllegalStateException.class); + expectedException.expectMessage("parent must be set on a doc associated to a IndexRelationType (see BaseDoc#setParent(String))"); + + doc.getFields(); + } + + @Test + public void getFields_contains_join_field_and_indexType_field_when_setParent_has_been_called_on_IndexRelationType() { + Index index = Index.withRelations("foo"); + IndexType.IndexRelationType relationType = IndexType.relation(IndexType.main(index, "bar"), "donut"); + BaseDoc doc = new BaseDoc(relationType) { + { + setParent("miam"); + } + + @Override + public String getId() { + throw new UnsupportedOperationException("getId not implemented"); + } + + }; + + Map<String, Object> fields = doc.getFields(); + + assertThat((Map) fields.get(index.getJoinField())) + .isEqualTo(ImmutableMap.of("name", relationType.getName(), "parent", "miam")); + assertThat(fields.get("indexType")).isEqualTo(relationType.getName()); + } +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/startup/GeneratePluginIndexTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/startup/GeneratePluginIndexTest.java new file mode 100644 index 00000000000..fb585a8565b --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/startup/GeneratePluginIndexTest.java @@ -0,0 +1,88 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.startup; + +import java.io.File; +import java.io.IOException; +import java.util.List; +import org.apache.commons.io.FileUtils; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.sonar.core.platform.PluginInfo; +import org.sonar.server.platform.ServerFileSystem; +import org.sonar.server.plugins.InstalledPlugin; +import org.sonar.server.plugins.InstalledPlugin.FileAndMd5; +import org.sonar.server.plugins.PluginFileSystem; + +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class GeneratePluginIndexTest { + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + private ServerFileSystem serverFileSystem = mock(ServerFileSystem.class); + private PluginFileSystem pluginFileSystem = mock(PluginFileSystem.class); + private File index; + + @Before + public void createIndexFile() throws IOException { + index = temp.newFile(); + when(serverFileSystem.getPluginIndex()).thenReturn(index); + } + + @Test + public void shouldWriteIndex() throws IOException { + InstalledPlugin javaPlugin = newInstalledPlugin("java", true); + InstalledPlugin gitPlugin = newInstalledPlugin("scmgit", false); + when(pluginFileSystem.getInstalledFiles()).thenReturn(asList(javaPlugin, gitPlugin)); + + GeneratePluginIndex underTest = new GeneratePluginIndex(serverFileSystem, pluginFileSystem); + underTest.start(); + + List<String> lines = FileUtils.readLines(index); + assertThat(lines).containsExactly( + "java,true," + javaPlugin.getLoadedJar().getFile().getName() + "|" + javaPlugin.getLoadedJar().getMd5(), + "scmgit,false," + gitPlugin.getLoadedJar().getFile().getName() + "|" + gitPlugin.getLoadedJar().getMd5()); + + underTest.stop(); + } + + @Test(expected = IllegalStateException.class) + public void shouldThrowWhenUnableToWrite() throws IOException { + File wrongParent = temp.newFile(); + wrongParent.createNewFile(); + File wrongIndex = new File(wrongParent, "index.txt"); + when(serverFileSystem.getPluginIndex()).thenReturn(wrongIndex); + + new GeneratePluginIndex(serverFileSystem, pluginFileSystem).start(); + } + + private InstalledPlugin newInstalledPlugin(String key, boolean supportSonarLint) throws IOException { + FileAndMd5 jar = new FileAndMd5(temp.newFile()); + PluginInfo pluginInfo = new PluginInfo(key).setJarFile(jar.getFile()).setSonarLintSupported(supportSonarLint); + return new InstalledPlugin(pluginInfo, jar, null); + } +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/startup/LogServerIdTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/startup/LogServerIdTest.java new file mode 100644 index 00000000000..073e9577710 --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/startup/LogServerIdTest.java @@ -0,0 +1,50 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.startup; + +import org.junit.Rule; +import org.junit.Test; +import org.sonar.api.platform.Server; +import org.sonar.api.utils.log.LogTester; +import org.sonar.api.utils.log.LoggerLevel; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class LogServerIdTest { + + @Rule + public LogTester logTester = new LogTester(); + + @Test + public void log_server_id_at_startup() { + Server server = mock(Server.class); + when(server.getId()).thenReturn("foo"); + + LogServerId underTest = new LogServerId(server); + + underTest.start(); + assertThat(logTester.logs(LoggerLevel.INFO)).contains("Server ID: foo"); + + // do not fail + underTest.stop(); + } +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/startup/RegisterMetricsTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/startup/RegisterMetricsTest.java new file mode 100644 index 00000000000..b5fc51496cd --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/startup/RegisterMetricsTest.java @@ -0,0 +1,238 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.startup; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.stream.IntStream; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.api.measures.CoreMetrics; +import org.sonar.api.measures.Metric; +import org.sonar.api.measures.Metrics; +import org.sonar.api.utils.System2; +import org.sonar.db.DbClient; +import org.sonar.db.DbTester; +import org.sonar.db.metric.MetricDto; + +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; +import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex; + +public class RegisterMetricsTest { + + @Rule + public DbTester dbTester = DbTester.create(System2.INSTANCE); + + private DbClient dbClient = dbTester.getDbClient(); + + /** + * Insert new metrics, including custom metrics + */ + @Test + public void insert_new_metrics() { + Metric m1 = new Metric.Builder("m1", "One", Metric.ValueType.FLOAT) + .setDescription("desc1") + .setDirection(1) + .setQualitative(true) + .setDomain("domain1") + .setUserManaged(false) + .create(); + Metric custom = new Metric.Builder("custom", "Custom", Metric.ValueType.FLOAT) + .setDescription("This is a custom metric") + .setUserManaged(true) + .create(); + + RegisterMetrics register = new RegisterMetrics(dbClient); + register.register(asList(m1, custom)); + + Map<String, MetricDto> metricsByKey = selectAllMetrics(); + assertThat(metricsByKey).hasSize(2); + assertEquals(m1, metricsByKey.get("m1")); + assertEquals(custom, metricsByKey.get("custom")); + } + + /** + * Update existing metrics, except if custom metric + */ + @Test + public void update_non_custom_metrics() { + dbTester.measures().insertMetric(t -> t.setKey("m1") + .setShortName("name") + .setValueType(Metric.ValueType.INT.name()) + .setDescription("old desc") + .setDomain("old domain") + .setShortName("old short name") + .setQualitative(false) + .setUserManaged(false) + .setEnabled(true) + .setOptimizedBestValue(false) + .setDirection(1) + .setHidden(false)); + MetricDto customMetric = dbTester.measures().insertMetric(t -> t.setKey("custom") + .setValueType(Metric.ValueType.FLOAT.name()) + .setDescription("old desc") + .setShortName("Custom") + .setQualitative(false) + .setUserManaged(true) + .setEnabled(true) + .setOptimizedBestValue(false) + .setDirection(0) + .setHidden(false) + .setDecimalScale(1)); + + RegisterMetrics register = new RegisterMetrics(dbClient); + Metric m1 = new Metric.Builder("m1", "New name", Metric.ValueType.FLOAT) + .setDescription("new description") + .setDirection(-1) + .setQualitative(true) + .setDomain("new domain") + .setUserManaged(false) + .setDecimalScale(3) + .setHidden(true) + .create(); + Metric custom = new Metric.Builder("custom", "New custom", Metric.ValueType.FLOAT) + .setDescription("New description of custom metric") + .setUserManaged(true) + .create(); + register.register(asList(m1, custom)); + + Map<String, MetricDto> metricsByKey = selectAllMetrics(); + assertThat(metricsByKey).hasSize(2); + assertEquals(m1, metricsByKey.get("m1")); + MetricDto actual = metricsByKey.get("custom"); + assertThat(actual.getKey()).isEqualTo(custom.getKey()); + assertThat(actual.getShortName()).isEqualTo(customMetric.getShortName()); + assertThat(actual.getValueType()).isEqualTo(customMetric.getValueType()); + assertThat(actual.getDescription()).isEqualTo(customMetric.getDescription()); + assertThat(actual.getDirection()).isEqualTo(customMetric.getDirection()); + assertThat(actual.isQualitative()).isEqualTo(customMetric.isQualitative()); + assertThat(actual.isUserManaged()).isEqualTo(customMetric.isUserManaged()); + } + + @Test + public void disable_undefined_metrics() { + Random random = new Random(); + int count = 1 + random.nextInt(10); + IntStream.range(0, count) + .forEach(t -> dbTester.measures().insertMetric(m -> m.setEnabled(random.nextBoolean()).setUserManaged(false))); + + RegisterMetrics register = new RegisterMetrics(dbClient); + register.register(Collections.emptyList()); + + assertThat(selectAllMetrics().values().stream()) + .extracting(MetricDto::isEnabled) + .containsOnly(IntStream.range(0, count).mapToObj(t -> false).toArray(Boolean[]::new)); + } + + @Test + public void enable_disabled_metrics() { + MetricDto enabledMetric = dbTester.measures().insertMetric(t -> t.setEnabled(true).setUserManaged(false)); + MetricDto disabledMetric = dbTester.measures().insertMetric(t -> t.setEnabled(false).setUserManaged(false)); + + RegisterMetrics register = new RegisterMetrics(dbClient); + register.register(asList(builderOf(enabledMetric).create(), builderOf(disabledMetric).create())); + + assertThat(selectAllMetrics().values()) + .extracting(MetricDto::isEnabled) + .containsOnly(true, true); + } + + @Test + public void does_not_enable_disabled_custom_metrics() { + MetricDto enabledMetric = dbTester.measures().insertMetric(t -> t.setEnabled(true).setUserManaged(true)); + MetricDto disabledMetric = dbTester.measures().insertMetric(t -> t.setEnabled(false).setUserManaged(true)); + + RegisterMetrics register = new RegisterMetrics(dbClient); + register.register(asList(builderOf(enabledMetric).create(), builderOf(disabledMetric).create())); + + assertThat(selectAllMetrics().values()) + .extracting(MetricDto::getKey, MetricDto::isEnabled) + .containsOnly( + tuple(enabledMetric.getKey(), true), + tuple(disabledMetric.getKey(), false)); + } + + @Test + public void insert_core_metrics() { + RegisterMetrics register = new RegisterMetrics(dbClient); + register.start(); + + assertThat(dbTester.countRowsOfTable("metrics")).isEqualTo(CoreMetrics.getMetrics().size()); + } + + @Test(expected = IllegalStateException.class) + public void fail_if_duplicated_plugin_metrics() { + Metrics plugin1 = new TestMetrics(new Metric.Builder("m1", "In first plugin", Metric.ValueType.FLOAT).create()); + Metrics plugin2 = new TestMetrics(new Metric.Builder("m1", "In second plugin", Metric.ValueType.FLOAT).create()); + + new RegisterMetrics(dbClient, new Metrics[] {plugin1, plugin2}).start(); + } + + @Test(expected = IllegalStateException.class) + public void fail_if_plugin_duplicates_core_metric() { + Metrics plugin = new TestMetrics(new Metric.Builder("ncloc", "In plugin", Metric.ValueType.FLOAT).create()); + + new RegisterMetrics(dbClient, new Metrics[] {plugin}).start(); + } + + private class TestMetrics implements Metrics { + private final List<Metric> metrics; + + public TestMetrics(Metric... metrics) { + this.metrics = asList(metrics); + } + + @Override + public List<Metric> getMetrics() { + return metrics; + } + } + + private Map<String, MetricDto> selectAllMetrics() { + return dbTester.getDbClient().metricDao().selectAll(dbTester.getSession()) + .stream() + .collect(uniqueIndex(MetricDto::getKey)); + } + + private void assertEquals(Metric expected, MetricDto actual) { + assertThat(actual.getKey()).isEqualTo(expected.getKey()); + assertThat(actual.getShortName()).isEqualTo(expected.getName()); + assertThat(actual.getValueType()).isEqualTo(expected.getType().name()); + assertThat(actual.getDescription()).isEqualTo(expected.getDescription()); + assertThat(actual.getDirection()).isEqualTo(expected.getDirection()); + assertThat(actual.isQualitative()).isEqualTo(expected.getQualitative()); + assertThat(actual.isUserManaged()).isEqualTo(expected.getUserManaged()); + } + + private static Metric.Builder builderOf(MetricDto enabledMetric) { + return new Metric.Builder(enabledMetric.getKey(), enabledMetric.getShortName(), Metric.ValueType.valueOf(enabledMetric.getValueType())) + .setDescription(enabledMetric.getDescription()) + .setDirection(enabledMetric.getDirection()) + .setQualitative(enabledMetric.isQualitative()) + .setQualitative(enabledMetric.isQualitative()) + .setDomain(enabledMetric.getDomain()) + .setUserManaged(enabledMetric.isUserManaged()) + .setHidden(enabledMetric.isHidden()); + } +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/startup/RegisterPermissionTemplatesTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/startup/RegisterPermissionTemplatesTest.java new file mode 100644 index 00000000000..b7683ee184d --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/startup/RegisterPermissionTemplatesTest.java @@ -0,0 +1,212 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.startup; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.resources.Qualifiers; +import org.sonar.api.resources.ResourceTypes; +import org.sonar.api.security.DefaultGroups; +import org.sonar.api.utils.System2; +import org.sonar.api.utils.log.LogTester; +import org.sonar.api.utils.log.LoggerLevel; +import org.sonar.api.web.UserRole; +import org.sonar.db.DbTester; +import org.sonar.db.organization.DefaultTemplates; +import org.sonar.db.permission.OrganizationPermission; +import org.sonar.db.permission.template.PermissionTemplateDto; +import org.sonar.db.permission.template.PermissionTemplateGroupDto; +import org.sonar.db.user.GroupDto; +import org.sonar.server.organization.DefaultOrganizationProvider; +import org.sonar.server.organization.TestDefaultOrganizationProvider; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.sonar.db.permission.template.PermissionTemplateTesting.newPermissionTemplateDto; + +public class RegisterPermissionTemplatesTest { + private static final String DEFAULT_TEMPLATE_UUID = "default_template"; + + @Rule + public DbTester db = DbTester.create(System2.INSTANCE); + @Rule + public LogTester logTester = new LogTester(); + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + private DefaultOrganizationProvider defaultOrganizationProvider = TestDefaultOrganizationProvider.from(db); + private ResourceTypes resourceTypes = mock(ResourceTypes.class); + private RegisterPermissionTemplates underTest = new RegisterPermissionTemplates(db.getDbClient(), defaultOrganizationProvider); + + @Test + public void fail_with_ISE_if_default_template_must_be_created_and_no_default_group_is_defined() { + expectedException.expect(IllegalStateException.class); + expectedException.expectMessage("Default group for organization " + db.getDefaultOrganization().getUuid() + " is not defined"); + + underTest.start(); + } + + @Test + public void fail_with_ISE_if_default_template_must_be_created_and_default_group_does_not_exist() { + setDefaultGroupId(new GroupDto().setId(22)); + + expectedException.expect(IllegalStateException.class); + expectedException.expectMessage("Default group with id 22 for organization " + db.getDefaultOrganization().getUuid() + " doesn't exist"); + + underTest.start(); + } + + @Test + public void insert_default_permission_template_if_fresh_install_without_governance() { + GroupDto defaultGroup = createAndSetDefaultGroup(); + db.users().insertGroup(db.getDefaultOrganization(), DefaultGroups.ADMINISTRATORS); + + when(resourceTypes.isQualifierPresent(eq(Qualifiers.APP))).thenReturn(false); + when(resourceTypes.isQualifierPresent(eq(Qualifiers.VIEW))).thenReturn(false); + underTest.start(); + + PermissionTemplateDto defaultTemplate = selectTemplate(); + assertThat(defaultTemplate.getName()).isEqualTo("Default template"); + + List<PermissionTemplateGroupDto> groupPermissions = selectGroupPermissions(defaultTemplate); + assertThat(groupPermissions).hasSize(7); + expectGroupPermission(groupPermissions, UserRole.ADMIN, DefaultGroups.ADMINISTRATORS); + expectGroupPermission(groupPermissions, OrganizationPermission.APPLICATION_CREATOR.getKey(), DefaultGroups.ADMINISTRATORS); + expectGroupPermission(groupPermissions, OrganizationPermission.PORTFOLIO_CREATOR.getKey(), DefaultGroups.ADMINISTRATORS); + expectGroupPermission(groupPermissions, UserRole.CODEVIEWER, defaultGroup.getName()); + expectGroupPermission(groupPermissions, UserRole.USER, defaultGroup.getName()); + expectGroupPermission(groupPermissions, UserRole.ISSUE_ADMIN, defaultGroup.getName()); + expectGroupPermission(groupPermissions, UserRole.SECURITYHOTSPOT_ADMIN, defaultGroup.getName()); + + verifyDefaultTemplates(); + + assertThat(logTester.logs(LoggerLevel.ERROR)).isEmpty(); + } + + @Test + public void insert_default_permission_template_if_fresh_install_with_governance() { + GroupDto defaultGroup = createAndSetDefaultGroup(); + db.users().insertGroup(db.getDefaultOrganization(), DefaultGroups.ADMINISTRATORS); + + when(resourceTypes.isQualifierPresent(eq(Qualifiers.APP))).thenReturn(true); + when(resourceTypes.isQualifierPresent(eq(Qualifiers.VIEW))).thenReturn(true); + underTest.start(); + + PermissionTemplateDto defaultTemplate = selectTemplate(); + assertThat(defaultTemplate.getName()).isEqualTo("Default template"); + + List<PermissionTemplateGroupDto> groupPermissions = selectGroupPermissions(defaultTemplate); + assertThat(groupPermissions).hasSize(7); + expectGroupPermission(groupPermissions, UserRole.ADMIN, DefaultGroups.ADMINISTRATORS); + expectGroupPermission(groupPermissions, OrganizationPermission.APPLICATION_CREATOR.getKey(), DefaultGroups.ADMINISTRATORS); + expectGroupPermission(groupPermissions, OrganizationPermission.PORTFOLIO_CREATOR.getKey(), DefaultGroups.ADMINISTRATORS); + expectGroupPermission(groupPermissions, UserRole.CODEVIEWER, defaultGroup.getName()); + expectGroupPermission(groupPermissions, UserRole.USER, defaultGroup.getName()); + expectGroupPermission(groupPermissions, UserRole.ISSUE_ADMIN, defaultGroup.getName()); + expectGroupPermission(groupPermissions, UserRole.SECURITYHOTSPOT_ADMIN, defaultGroup.getName()); + + verifyDefaultTemplates(); + + assertThat(logTester.logs(LoggerLevel.ERROR)).isEmpty(); + } + + @Test + public void ignore_administrators_permissions_if_group_does_not_exist() { + GroupDto defaultGroup = createAndSetDefaultGroup(); + + underTest.start(); + + PermissionTemplateDto defaultTemplate = selectTemplate(); + assertThat(defaultTemplate.getName()).isEqualTo("Default template"); + + List<PermissionTemplateGroupDto> groupPermissions = selectGroupPermissions(defaultTemplate); + assertThat(groupPermissions).hasSize(4); + expectGroupPermission(groupPermissions, UserRole.CODEVIEWER, defaultGroup.getName()); + expectGroupPermission(groupPermissions, UserRole.USER, defaultGroup.getName()); + expectGroupPermission(groupPermissions, UserRole.ISSUE_ADMIN, defaultGroup.getName()); + expectGroupPermission(groupPermissions, UserRole.SECURITYHOTSPOT_ADMIN, defaultGroup.getName()); + + verifyDefaultTemplates(); + + assertThat(logTester.logs(LoggerLevel.ERROR)).contains("Cannot setup default permission for group: sonar-administrators"); + } + + @Test + public void do_not_create_default_template_if_already_exists_but_register_when_it_is_not() { + db.permissionTemplates().insertTemplate(newPermissionTemplateDto() + .setOrganizationUuid(db.getDefaultOrganization().getUuid()) + .setUuid(DEFAULT_TEMPLATE_UUID)); + + underTest.start(); + + verifyDefaultTemplates(); + } + + @Test + public void do_not_fail_if_default_template_exists_and_is_registered() { + PermissionTemplateDto projectTemplate = db.permissionTemplates().insertTemplate(newPermissionTemplateDto() + .setOrganizationUuid(db.getDefaultOrganization().getUuid()) + .setUuid(DEFAULT_TEMPLATE_UUID)); + db.organizations().setDefaultTemplates(projectTemplate, null, null); + + underTest.start(); + + verifyDefaultTemplates(); + } + + private PermissionTemplateDto selectTemplate() { + return db.getDbClient().permissionTemplateDao().selectByUuid(db.getSession(), DEFAULT_TEMPLATE_UUID); + } + + private List<PermissionTemplateGroupDto> selectGroupPermissions(PermissionTemplateDto template) { + return db.getDbClient().permissionTemplateDao().selectGroupPermissionsByTemplateId(db.getSession(), template.getId()); + } + + private void expectGroupPermission(List<PermissionTemplateGroupDto> groupPermissions, String expectedPermission, + String expectedGroupName) { + assertThat( + groupPermissions.stream().anyMatch(gp -> gp.getPermission().equals(expectedPermission) && Objects.equals(gp.getGroupName(), expectedGroupName))) + .isTrue(); + } + + private void verifyDefaultTemplates() { + Optional<DefaultTemplates> defaultTemplates = db.getDbClient().organizationDao().getDefaultTemplates(db.getSession(), db.getDefaultOrganization().getUuid()); + assertThat(defaultTemplates) + .isPresent(); + assertThat(defaultTemplates.get().getProjectUuid()).isEqualTo(DEFAULT_TEMPLATE_UUID); + } + + private void setDefaultGroupId(GroupDto defaultGroup) { + db.getDbClient().organizationDao().setDefaultGroupId(db.getSession(), db.getDefaultOrganization().getUuid(), defaultGroup); + db.commit(); + } + + private GroupDto createAndSetDefaultGroup() { + GroupDto res = db.users().insertGroup(db.getDefaultOrganization()); + setDefaultGroupId(res); + return res; + } +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/startup/RegisterPluginsTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/startup/RegisterPluginsTest.java new file mode 100644 index 00000000000..9fed6eef493 --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/startup/RegisterPluginsTest.java @@ -0,0 +1,144 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.startup; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import javax.annotation.Nullable; +import org.apache.commons.io.FileUtils; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.sonar.api.utils.System2; +import org.sonar.core.platform.PluginInfo; +import org.sonar.core.util.UuidFactory; +import org.sonar.db.DbClient; +import org.sonar.db.DbTester; +import org.sonar.db.plugin.PluginDto; +import org.sonar.server.plugins.InstalledPlugin; +import org.sonar.server.plugins.PluginFileSystem; + +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex; + +public class RegisterPluginsTest { + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + @Rule + public DbTester dbTester = DbTester.create(System2.INSTANCE); + + private final long now = 12345L; + private DbClient dbClient = dbTester.getDbClient(); + private PluginFileSystem pluginFileSystem = mock(PluginFileSystem.class); + private UuidFactory uuidFactory = mock(UuidFactory.class); + private System2 system2 = mock(System2.class); + + @Before + public void setUp() { + when(system2.now()).thenReturn(now).thenThrow(new IllegalStateException("Should be called only once")); + } + + /** + * Insert new plugins + */ + @Test + public void insert_new_plugins() throws IOException { + File fakeJavaJar = temp.newFile(); + FileUtils.write(fakeJavaJar, "fakejava", StandardCharsets.UTF_8); + File fakeJavaCustomJar = temp.newFile(); + FileUtils.write(fakeJavaCustomJar, "fakejavacustom", StandardCharsets.UTF_8); + when(pluginFileSystem.getInstalledFiles()).thenReturn(asList( + newPlugin("java", fakeJavaJar, null), + newPlugin("javacustom", fakeJavaCustomJar, "java"))); + when(uuidFactory.create()).thenReturn("a").thenReturn("b").thenThrow(new IllegalStateException("Should be called only twice")); + RegisterPlugins register = new RegisterPlugins(pluginFileSystem, dbClient, uuidFactory, system2); + register.start(); + + Map<String, PluginDto> pluginsByKey = selectAllPlugins(); + assertThat(pluginsByKey).hasSize(2); + verify(pluginsByKey.get("java"), null, "bd451e47a1aa76e73da0359cef63dd63", now, now); + verify(pluginsByKey.get("javacustom"), "java", "de9b2de3ddc0680904939686c0dba5be", now, now); + + register.stop(); + } + + /** + * Update existing plugins, only when checksum is different and don't remove uninstalled plugins + */ + @Test + public void update_only_changed_plugins() throws IOException { + dbClient.pluginDao().insert(dbTester.getSession(), new PluginDto() + .setUuid("a") + .setKee("java") + .setBasePluginKey(null) + .setFileHash("bd451e47a1aa76e73da0359cef63dd63") + .setCreatedAt(1L) + .setUpdatedAt(1L)); + dbClient.pluginDao().insert(dbTester.getSession(), new PluginDto() + .setUuid("b") + .setKee("javacustom") + .setBasePluginKey("java") + .setFileHash("de9b2de3ddc0680904939686c0dba5be") + .setCreatedAt(1L) + .setUpdatedAt(1L)); + dbTester.commit(); + + File fakeJavaCustomJar = temp.newFile(); + FileUtils.write(fakeJavaCustomJar, "fakejavacustomchanged", StandardCharsets.UTF_8); + when(pluginFileSystem.getInstalledFiles()).thenReturn(asList( + newPlugin("javacustom", fakeJavaCustomJar, "java2"))); + + new RegisterPlugins(pluginFileSystem, dbClient, uuidFactory, system2).start(); + + Map<String, PluginDto> pluginsByKey = selectAllPlugins(); + assertThat(pluginsByKey).hasSize(2); + verify(pluginsByKey.get("java"), null, "bd451e47a1aa76e73da0359cef63dd63", 1L, 1L); + verify(pluginsByKey.get("javacustom"), "java2", "d22091cff5155e892cfe2f9dab51f811", 1L, now); + } + + private static InstalledPlugin newPlugin(String key, File file, @Nullable String basePlugin) { + InstalledPlugin.FileAndMd5 jar = new InstalledPlugin.FileAndMd5(file); + PluginInfo info = new PluginInfo(key) + .setBasePlugin(basePlugin) + .setJarFile(file); + return new InstalledPlugin(info, jar, null); + } + + private Map<String, PluginDto> selectAllPlugins() { + return dbTester.getDbClient().pluginDao().selectAll(dbTester.getSession()) + .stream() + .collect(uniqueIndex(PluginDto::getKee)); + } + + private void verify(PluginDto java, @Nullable String basePluginKey, String fileHash, @Nullable Long createdAt, long updatedAt) { + assertThat(java.getBasePluginKey()).isEqualTo(basePluginKey); + assertThat(java.getFileHash()).isEqualTo(fileHash); + assertThat(java.getCreatedAt()).isEqualTo(createdAt); + assertThat(java.getUpdatedAt()).isEqualTo(updatedAt); + } + +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/startup/RenameDeprecatedPropertyKeysTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/startup/RenameDeprecatedPropertyKeysTest.java new file mode 100644 index 00000000000..0edd9f2d27e --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/startup/RenameDeprecatedPropertyKeysTest.java @@ -0,0 +1,49 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.startup; + +import org.junit.Test; +import org.sonar.api.Properties; +import org.sonar.api.Property; +import org.sonar.api.config.PropertyDefinitions; +import org.sonar.db.property.PropertiesDao; + +import static org.mockito.Mockito.*; + +public class RenameDeprecatedPropertyKeysTest { + @Test + public void should_rename_deprecated_keys() { + PropertiesDao dao = mock(PropertiesDao.class); + PropertyDefinitions definitions = new PropertyDefinitions(FakeExtension.class); + RenameDeprecatedPropertyKeys task = new RenameDeprecatedPropertyKeys(dao, definitions); + task.start(); + + verify(dao).renamePropertyKey("old_key", "new_key"); + verifyNoMoreInteractions(dao); + } + + @Properties({ + @Property(key = "new_key", deprecatedKey = "old_key", name = "Name"), + @Property(key = "other", name = "Other") + }) + public static class FakeExtension { + + } +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/telemetry/FakeServer.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/telemetry/FakeServer.java new file mode 100644 index 00000000000..b1aa8a872ce --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/telemetry/FakeServer.java @@ -0,0 +1,98 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.telemetry; + +import java.io.File; +import java.util.Date; +import javax.annotation.CheckForNull; +import org.sonar.api.platform.Server; + +import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric; + +class FakeServer extends Server { + private String id; + private String version; + + public FakeServer() { + this.id = randomAlphanumeric(20); + this.version = randomAlphanumeric(10); + } + + @Override + public String getId() { + return id; + } + + FakeServer setId(String id) { + this.id = id; + return this; + } + + @CheckForNull + @Override + public String getPermanentServerId() { + return null; + } + + @Override + public String getVersion() { + return this.version; + } + + public FakeServer setVersion(String version) { + this.version = version; + return this; + } + + @Override + public Date getStartedAt() { + return null; + } + + @Override + public File getRootDir() { + return null; + } + + @Override + public String getContextPath() { + return null; + } + + @Override + public String getPublicRootUrl() { + return null; + } + + @Override + public boolean isDev() { + return false; + } + + @Override + public boolean isSecured() { + return false; + } + + @Override + public String getURL() { + return null; + } +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/telemetry/TelemetryClientTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/telemetry/TelemetryClientTest.java new file mode 100644 index 00000000000..60fdc81e5d6 --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/telemetry/TelemetryClientTest.java @@ -0,0 +1,82 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.telemetry; + +import java.io.IOException; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okio.Buffer; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.sonar.api.config.internal.MapSettings; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.sonar.process.ProcessProperties.Property.SONAR_TELEMETRY_URL; + +public class TelemetryClientTest { + + private static final String JSON = "{\"key\":\"value\"}"; + private static final String TELEMETRY_URL = "https://telemetry.com/url"; + + private OkHttpClient okHttpClient = mock(OkHttpClient.class, RETURNS_DEEP_STUBS); + private MapSettings settings = new MapSettings(); + + private TelemetryClient underTest = new TelemetryClient(okHttpClient, settings.asConfig()); + + @Test + public void upload() throws IOException { + ArgumentCaptor<Request> requestCaptor = ArgumentCaptor.forClass(Request.class); + settings.setProperty(SONAR_TELEMETRY_URL.getKey(), TELEMETRY_URL); + underTest.start(); + + underTest.upload(JSON); + + verify(okHttpClient).newCall(requestCaptor.capture()); + Request request = requestCaptor.getValue(); + assertThat(request.method()).isEqualTo("POST"); + assertThat(request.body().contentType()).isEqualTo(MediaType.parse("application/json; charset=utf-8")); + Buffer body = new Buffer(); + request.body().writeTo(body); + assertThat(body.readUtf8()).isEqualTo(JSON); + assertThat(request.url().toString()).isEqualTo(TELEMETRY_URL); + } + + @Test + public void opt_out() throws IOException { + ArgumentCaptor<Request> requestCaptor = ArgumentCaptor.forClass(Request.class); + settings.setProperty(SONAR_TELEMETRY_URL.getKey(), TELEMETRY_URL); + underTest.start(); + + underTest.optOut(JSON); + + verify(okHttpClient).newCall(requestCaptor.capture()); + Request request = requestCaptor.getValue(); + assertThat(request.method()).isEqualTo("DELETE"); + assertThat(request.body().contentType()).isEqualTo(MediaType.parse("application/json; charset=utf-8")); + Buffer body = new Buffer(); + request.body().writeTo(body); + assertThat(body.readUtf8()).isEqualTo(JSON); + assertThat(request.url().toString()).isEqualTo(TELEMETRY_URL); + } +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/telemetry/TelemetryDaemonTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/telemetry/TelemetryDaemonTest.java new file mode 100644 index 00000000000..dc57204baf2 --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/telemetry/TelemetryDaemonTest.java @@ -0,0 +1,376 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.telemetry; + +import java.io.IOException; +import java.sql.DatabaseMetaData; +import java.sql.SQLException; +import java.util.List; +import java.util.Optional; +import java.util.stream.IntStream; +import org.junit.After; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.sonar.api.config.internal.MapSettings; +import org.sonar.api.impl.utils.TestSystem2; +import org.sonar.api.utils.log.LogTester; +import org.sonar.api.utils.log.LoggerLevel; +import org.sonar.core.platform.EditionProvider; +import org.sonar.core.platform.PlatformEditionProvider; +import org.sonar.core.platform.PluginInfo; +import org.sonar.core.platform.PluginRepository; +import org.sonar.db.DbSession; +import org.sonar.db.DbTester; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.metric.MetricDto; +import org.sonar.server.es.EsTester; +import org.sonar.server.measure.index.ProjectMeasuresIndex; +import org.sonar.server.measure.index.ProjectMeasuresIndexer; +import org.sonar.server.organization.DefaultOrganizationProviderImpl; +import org.sonar.server.property.InternalProperties; +import org.sonar.server.property.MapInternalProperties; +import org.sonar.server.tester.UserSessionRule; +import org.sonar.server.user.index.UserIndex; +import org.sonar.server.user.index.UserIndexer; +import org.sonar.server.util.GlobalLockManager; +import org.sonar.updatecenter.common.Version; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptySet; +import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic; +import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.after; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.sonar.api.measures.CoreMetrics.COVERAGE_KEY; +import static org.sonar.api.measures.CoreMetrics.LINES_KEY; +import static org.sonar.api.measures.CoreMetrics.NCLOC_KEY; +import static org.sonar.api.measures.CoreMetrics.NCLOC_LANGUAGE_DISTRIBUTION_KEY; +import static org.sonar.api.utils.DateUtils.parseDate; +import static org.sonar.db.component.BranchType.LONG; +import static org.sonar.db.component.BranchType.SHORT; +import static org.sonar.process.ProcessProperties.Property.SONAR_TELEMETRY_ENABLE; +import static org.sonar.process.ProcessProperties.Property.SONAR_TELEMETRY_FREQUENCY_IN_SECONDS; +import static org.sonar.process.ProcessProperties.Property.SONAR_TELEMETRY_URL; +import static org.sonar.test.JsonAssert.assertJson; + +public class TelemetryDaemonTest { + + private static final long ONE_HOUR = 60 * 60 * 1_000L; + private static final long ONE_DAY = 24 * ONE_HOUR; + + @Rule + public UserSessionRule userSession = UserSessionRule.standalone(); + @Rule + public DbTester db = DbTester.create(); + @Rule + public EsTester es = EsTester.create(); + @Rule + public LogTester logger = new LogTester().setLevel(LoggerLevel.DEBUG); + + private TelemetryClient client = mock(TelemetryClient.class); + private InternalProperties internalProperties = spy(new MapInternalProperties()); + private final GlobalLockManager lockManager = mock(GlobalLockManager.class); + private FakeServer server = new FakeServer(); + private PluginRepository pluginRepository = mock(PluginRepository.class); + private TestSystem2 system2 = new TestSystem2().setNow(System.currentTimeMillis()); + private MapSettings settings = new MapSettings(); + private ProjectMeasuresIndexer projectMeasuresIndexer = new ProjectMeasuresIndexer(db.getDbClient(), es.client()); + private UserIndexer userIndexer = new UserIndexer(db.getDbClient(), es.client()); + private PlatformEditionProvider editionProvider = mock(PlatformEditionProvider.class); + + private final TelemetryDataLoader communityDataLoader = new TelemetryDataLoaderImpl(server, db.getDbClient(), pluginRepository, new UserIndex(es.client(), system2), + new ProjectMeasuresIndex(es.client(), null, system2), editionProvider, new DefaultOrganizationProviderImpl(db.getDbClient()), internalProperties, null); + private TelemetryDaemon communityUnderTest = new TelemetryDaemon(communityDataLoader, client, settings.asConfig(), internalProperties, lockManager, system2); + + private final LicenseReader licenseReader = mock(LicenseReader.class); + private final TelemetryDataLoader commercialDataLoader = new TelemetryDataLoaderImpl(server, db.getDbClient(), pluginRepository, new UserIndex(es.client(), system2), + new ProjectMeasuresIndex(es.client(), null, system2), editionProvider, new DefaultOrganizationProviderImpl(db.getDbClient()), internalProperties, licenseReader); + private TelemetryDaemon commercialUnderTest = new TelemetryDaemon(commercialDataLoader, client, settings.asConfig(), internalProperties, lockManager, system2); + + @After + public void tearDown() { + communityUnderTest.stop(); + } + + @Test + public void send_telemetry_data() throws IOException { + initTelemetrySettingsToDefaultValues(); + settings.setProperty("sonar.telemetry.frequencyInSeconds", "1"); + server.setId("AU-TpxcB-iU5OvuD2FL7"); + server.setVersion("7.5.4"); + List<PluginInfo> plugins = asList(newPlugin("java", "4.12.0.11033"), newPlugin("scmgit", "1.2"), new PluginInfo("other")); + when(pluginRepository.getPluginInfos()).thenReturn(plugins); + when(editionProvider.get()).thenReturn(Optional.of(EditionProvider.Edition.DEVELOPER)); + when(lockManager.tryLock(any(), anyInt())).thenReturn(true); + + IntStream.range(0, 3).forEach(i -> db.users().insertUser()); + db.users().insertUser(u -> u.setActive(false)); + userIndexer.indexOnStartup(emptySet()); + + MetricDto lines = db.measures().insertMetric(m -> m.setKey(LINES_KEY)); + MetricDto ncloc = db.measures().insertMetric(m -> m.setKey(NCLOC_KEY)); + MetricDto coverage = db.measures().insertMetric(m -> m.setKey(COVERAGE_KEY)); + MetricDto nclocDistrib = db.measures().insertMetric(m -> m.setKey(NCLOC_LANGUAGE_DISTRIBUTION_KEY)); + + ComponentDto project1 = db.components().insertMainBranch(db.getDefaultOrganization()); + ComponentDto project1Branch = db.components().insertProjectBranch(project1); + db.measures().insertLiveMeasure(project1, lines, m -> m.setValue(200d)); + db.measures().insertLiveMeasure(project1, ncloc, m -> m.setValue(100d)); + db.measures().insertLiveMeasure(project1, coverage, m -> m.setValue(80d)); + db.measures().insertLiveMeasure(project1, nclocDistrib, m -> m.setValue(null).setData("java=200;js=50")); + + ComponentDto project2 = db.components().insertMainBranch(db.getDefaultOrganization()); + db.measures().insertLiveMeasure(project2, lines, m -> m.setValue(300d)); + db.measures().insertLiveMeasure(project2, ncloc, m -> m.setValue(200d)); + db.measures().insertLiveMeasure(project2, coverage, m -> m.setValue(80d)); + db.measures().insertLiveMeasure(project2, nclocDistrib, m -> m.setValue(null).setData("java=300;kotlin=2500")); + projectMeasuresIndexer.indexOnStartup(emptySet()); + + communityUnderTest.start(); + + ArgumentCaptor<String> jsonCaptor = captureJson(); + String json = jsonCaptor.getValue(); + assertJson(json).ignoreFields("database").isSimilarTo(getClass().getResource("telemetry-example.json")); + assertJson(getClass().getResource("telemetry-example.json")).ignoreFields("database").isSimilarTo(json); + assertDatabaseMetadata(json); + assertThat(logger.logs(LoggerLevel.INFO)).contains("Sharing of SonarQube statistics is enabled."); + } + + private void assertDatabaseMetadata(String json) { + try (DbSession dbSession = db.getDbClient().openSession(false)) { + DatabaseMetaData metadata = dbSession.getConnection().getMetaData(); + assertJson(json).isSimilarTo("{\n" + + " \"database\": {\n" + + " \"name\": \"H2\",\n" + + " \"version\": \"" + metadata.getDatabaseProductVersion() + "\"\n" + + " }\n" + + "}"); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + @Test + public void take_biggest_long_living_branches() throws IOException { + initTelemetrySettingsToDefaultValues(); + when(lockManager.tryLock(any(), anyInt())).thenReturn(true); + settings.setProperty("sonar.telemetry.frequencyInSeconds", "1"); + server.setId("AU-TpxcB-iU5OvuD2FL7").setVersion("7.5.4"); + MetricDto ncloc = db.measures().insertMetric(m -> m.setKey(NCLOC_KEY)); + ComponentDto project = db.components().insertMainBranch(db.getDefaultOrganization()); + ComponentDto longBranch = db.components().insertProjectBranch(project, b -> b.setBranchType(LONG)); + ComponentDto shortBranch = db.components().insertProjectBranch(project, b -> b.setBranchType(SHORT)); + db.measures().insertLiveMeasure(project, ncloc, m -> m.setValue(10d)); + db.measures().insertLiveMeasure(longBranch, ncloc, m -> m.setValue(20d)); + db.measures().insertLiveMeasure(shortBranch, ncloc, m -> m.setValue(30d)); + projectMeasuresIndexer.indexOnStartup(emptySet()); + + communityUnderTest.start(); + + ArgumentCaptor<String> jsonCaptor = captureJson(); + assertJson(jsonCaptor.getValue()).isSimilarTo("{\n" + + " \"ncloc\": 20\n" + + "}\n"); + } + + @Test + public void send_data_via_client_at_startup_after_initial_delay() throws IOException { + initTelemetrySettingsToDefaultValues(); + when(lockManager.tryLock(any(), anyInt())).thenReturn(true); + settings.setProperty("sonar.telemetry.frequencyInSeconds", "1"); + communityUnderTest.start(); + + verify(client, timeout(2_000).atLeastOnce()).upload(anyString()); + } + + @Test + public void data_contains_no_license_type_on_community_edition() throws IOException { + initTelemetrySettingsToDefaultValues(); + when(lockManager.tryLock(any(), anyInt())).thenReturn(true); + settings.setProperty("sonar.telemetry.frequencyInSeconds", "1"); + + communityUnderTest.start(); + + ArgumentCaptor<String> jsonCaptor = captureJson(); + assertThat(jsonCaptor.getValue()).doesNotContain("licenseType"); + } + + @Test + public void data_contains_no_license_type_on_commercial_edition_if_no_license() throws IOException { + initTelemetrySettingsToDefaultValues(); + settings.setProperty("sonar.telemetry.frequencyInSeconds", "1"); + when(licenseReader.read()).thenReturn(Optional.empty()); + when(lockManager.tryLock(any(), anyInt())).thenReturn(true); + + commercialUnderTest.start(); + + ArgumentCaptor<String> jsonCaptor = captureJson(); + assertThat(jsonCaptor.getValue()).doesNotContain("licenseType"); + } + + @Test + public void data_has_license_type_on_commercial_edition_if_no_license() throws IOException { + String licenseType = randomAlphabetic(12); + initTelemetrySettingsToDefaultValues(); + settings.setProperty("sonar.telemetry.frequencyInSeconds", "1"); + LicenseReader.License license = mock(LicenseReader.License.class); + when(license.getType()).thenReturn(licenseType); + when(licenseReader.read()).thenReturn(Optional.of(license)); + when(lockManager.tryLock(any(), anyInt())).thenReturn(true); + + commercialUnderTest.start(); + + ArgumentCaptor<String> jsonCaptor = captureJson(); + assertJson(jsonCaptor.getValue()).isSimilarTo("{\n" + + " \"licenseType\": \"" + licenseType + "\"\n" + + "}\n"); + } + + @Test + public void check_if_should_send_data_periodically() throws IOException { + initTelemetrySettingsToDefaultValues(); + when(lockManager.tryLock(any(), anyInt())).thenReturn(true); + long now = system2.now(); + long sixDaysAgo = now - (ONE_DAY * 6L); + long sevenDaysAgo = now - (ONE_DAY * 7L); + internalProperties.write("telemetry.lastPing", String.valueOf(sixDaysAgo)); + settings.setProperty("sonar.telemetry.frequencyInSeconds", "1"); + communityUnderTest.start(); + verify(client, after(2_000).never()).upload(anyString()); + internalProperties.write("telemetry.lastPing", String.valueOf(sevenDaysAgo)); + + verify(client, timeout(2_000).atLeastOnce()).upload(anyString()); + } + + @Test + public void send_server_id_and_version() throws IOException { + initTelemetrySettingsToDefaultValues(); + when(lockManager.tryLock(any(), anyInt())).thenReturn(true); + settings.setProperty("sonar.telemetry.frequencyInSeconds", "1"); + String id = randomAlphanumeric(40); + String version = randomAlphanumeric(10); + server.setId(id); + server.setVersion(version); + communityUnderTest.start(); + + ArgumentCaptor<String> json = captureJson(); + assertThat(json.getValue()).contains(id, version); + } + + @Test + public void send_server_installation_date_and_installation_version() throws IOException { + initTelemetrySettingsToDefaultValues(); + when(lockManager.tryLock(any(), anyInt())).thenReturn(true); + settings.setProperty("sonar.telemetry.frequencyInSeconds", "1"); + String installationVersion = "7.9.BEST.LTS.EVER"; + Long installationDate = 1546300800000L; // 2019/01/01 + internalProperties.write(InternalProperties.INSTALLATION_DATE, String.valueOf(installationDate)); + internalProperties.write(InternalProperties.INSTALLATION_VERSION, installationVersion); + + communityUnderTest.start(); + + ArgumentCaptor<String> json = captureJson(); + assertThat(json.getValue()).contains(installationVersion, installationDate.toString()); + } + + @Test + public void do_not_send_server_installation_details_if_missing_property() throws IOException { + initTelemetrySettingsToDefaultValues(); + when(lockManager.tryLock(any(), anyInt())).thenReturn(true); + settings.setProperty("sonar.telemetry.frequencyInSeconds", "1"); + + communityUnderTest.start(); + + ArgumentCaptor<String> json = captureJson(); + assertThat(json.getValue()).doesNotContain("installationVersion", "installationDate"); + } + + @Test + public void do_not_send_data_if_last_ping_earlier_than_one_week_ago() throws IOException { + initTelemetrySettingsToDefaultValues(); + when(lockManager.tryLock(any(), anyInt())).thenReturn(true); + settings.setProperty("sonar.telemetry.frequencyInSeconds", "1"); + long now = system2.now(); + long sixDaysAgo = now - (ONE_DAY * 6L); + + internalProperties.write("telemetry.lastPing", String.valueOf(sixDaysAgo)); + communityUnderTest.start(); + + verify(client, after(2_000).never()).upload(anyString()); + } + + @Test + public void send_data_if_last_ping_is_one_week_ago() throws IOException { + initTelemetrySettingsToDefaultValues(); + when(lockManager.tryLock(any(), anyInt())).thenReturn(true); + settings.setProperty("sonar.telemetry.frequencyInSeconds", "1"); + long today = parseDate("2017-08-01").getTime(); + system2.setNow(today + 15 * ONE_HOUR); + long sevenDaysAgo = today - (ONE_DAY * 7L); + internalProperties.write("telemetry.lastPing", String.valueOf(sevenDaysAgo)); + reset(internalProperties); + + communityUnderTest.start(); + + verify(internalProperties, timeout(4_000)).write("telemetry.lastPing", String.valueOf(today)); + verify(client).upload(anyString()); + } + + @Test + public void opt_out_sent_once() throws IOException { + initTelemetrySettingsToDefaultValues(); + when(lockManager.tryLock(any(), anyInt())).thenReturn(true); + settings.setProperty("sonar.telemetry.frequencyInSeconds", "1"); + settings.setProperty("sonar.telemetry.enable", "false"); + communityUnderTest.start(); + communityUnderTest.start(); + + verify(client, after(2_000).never()).upload(anyString()); + verify(client, timeout(2_000).times(1)).optOut(anyString()); + assertThat(logger.logs(LoggerLevel.INFO)).contains("Sharing of SonarQube statistics is disabled."); + } + + private PluginInfo newPlugin(String key, String version) { + return new PluginInfo(key) + .setVersion(Version.create(version)); + } + + private void initTelemetrySettingsToDefaultValues() { + settings.setProperty(SONAR_TELEMETRY_ENABLE.getKey(), SONAR_TELEMETRY_ENABLE.getDefaultValue()); + settings.setProperty(SONAR_TELEMETRY_URL.getKey(), SONAR_TELEMETRY_URL.getDefaultValue()); + settings.setProperty(SONAR_TELEMETRY_FREQUENCY_IN_SECONDS.getKey(), SONAR_TELEMETRY_FREQUENCY_IN_SECONDS.getDefaultValue()); + } + + private ArgumentCaptor<String> captureJson() throws IOException { + ArgumentCaptor<String> jsonCaptor = ArgumentCaptor.forClass(String.class); + verify(client, timeout(2_000).atLeastOnce()).upload(jsonCaptor.capture()); + return jsonCaptor; + } +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/updatecenter/UpdateCenterModuleTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/updatecenter/UpdateCenterModuleTest.java new file mode 100644 index 00000000000..b7531b0f39b --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/updatecenter/UpdateCenterModuleTest.java @@ -0,0 +1,35 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.updatecenter; + +import org.junit.Test; +import org.sonar.core.platform.ComponentContainer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.core.platform.ComponentContainer.COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER; + +public class UpdateCenterModuleTest { + @Test + public void verify_count_of_added_components() { + ComponentContainer container = new ComponentContainer(); + new UpdateCenterModule().configure(container); + assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 2); + } +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/util/DateCollectorTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/util/DateCollectorTest.java new file mode 100644 index 00000000000..e3d0146c112 --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/util/DateCollectorTest.java @@ -0,0 +1,45 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.util; + +import org.junit.Test; +import org.sonar.api.utils.DateUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +public class DateCollectorTest { + + DateCollector collector = new DateCollector(); + + @Test + public void max_is_zero_if_no_dates() { + assertThat(collector.getMax()).isEqualTo(0L); + } + + @Test + public void max() { + collector.add(DateUtils.parseDate("2013-06-01")); + collector.add(null); + collector.add(DateUtils.parseDate("2014-01-01")); + collector.add(DateUtils.parseDate("2013-08-01")); + + assertThat(collector.getMax()).isEqualTo(DateUtils.parseDateQuietly("2014-01-01").getTime()); + } +} diff --git a/server/sonar-webserver-core/src/test/resources/logback-test.xml b/server/sonar-webserver-core/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..3e34b0f9fc8 --- /dev/null +++ b/server/sonar-webserver-core/src/test/resources/logback-test.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<configuration debug="false"> + <contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator"/> + + <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> + <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> + <pattern> + %d{yyyy.MM.dd HH:mm:ss} %-5level %msg%n + </pattern> + </encoder> + </appender> + + <root> + <level value="INFO"/> + <appender-ref ref="CONSOLE"/> + </root> + + <logger name="ch.qos.logback"> + <level value="WARN"/> + </logger> + + <logger name="okhttp3.mockwebserver"> + <level value="WARN"/> + </logger> + +</configuration> diff --git a/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtModelPluginRepositoryTest/csharp-model.xml b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtModelPluginRepositoryTest/csharp-model.xml new file mode 100644 index 00000000000..e4569a2a7bf --- /dev/null +++ b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtModelPluginRepositoryTest/csharp-model.xml @@ -0,0 +1,25 @@ +<sqale> + <chc> + <key>USABILITY</key> + <name>Usability</name> + <desc>Estimate usability</desc> + </chc> + <chc> + <key>EFFICIENCY</key> + <name>Efficiency</name> + <chc> + <rule-repo>gendarme</rule-repo> + <rule-key>EnsureLocalDisposalRule</rule-key> + <prop> + <key>remediationFactor</key> + <val>0.125</val> + <txt>d</txt> + </prop> + <prop> + <key>remediationFunction</key> + <txt>linear</txt> + </prop> + </chc> + </chc> + +</sqale>
\ No newline at end of file diff --git a/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtModelPluginRepositoryTest/java-model.xml b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtModelPluginRepositoryTest/java-model.xml new file mode 100644 index 00000000000..0b37f562107 --- /dev/null +++ b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtModelPluginRepositoryTest/java-model.xml @@ -0,0 +1,25 @@ +<sqale> + <chc> + <key>USABILITY</key> + <name>Usability</name> + <desc>Estimate usability</desc> + </chc> + <chc> + <key>EFFICIENCY</key> + <name>Efficiency</name> + <chc> + <rule-repo>squid-cobol</rule-repo> + <rule-key>CheckLoop</rule-key> + <prop> + <key>remediationFactor</key> + <val>0.125</val> + <txt>d</txt> + </prop> + <prop> + <key>remediationFunction</key> + <txt>linear</txt> + </prop> + </chc> + </chc> + +</sqale>
\ No newline at end of file diff --git a/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtModelXMLExporterTest/export_xml.xml b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtModelXMLExporterTest/export_xml.xml new file mode 100644 index 00000000000..ef18e12ef83 --- /dev/null +++ b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtModelXMLExporterTest/export_xml.xml @@ -0,0 +1,20 @@ +<sqale> + <chc> + <rule-repo>checkstyle</rule-repo> + <rule-key>Regexp</rule-key> + <prop> + <key>remediationFunction</key> + <txt>LINEAR_OFFSET</txt> + </prop> + <prop> + <key>remediationFactor</key> + <val>3</val> + <txt>d</txt> + </prop> + <prop> + <key>offset</key> + <val>15</val> + <txt>min</txt> + </prop> + </chc> +</sqale> diff --git a/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/convert_constant_per_issue_with_coefficient_by_constant_per_issue_with_offset.xml b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/convert_constant_per_issue_with_coefficient_by_constant_per_issue_with_offset.xml new file mode 100644 index 00000000000..00deb4d0299 --- /dev/null +++ b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/convert_constant_per_issue_with_coefficient_by_constant_per_issue_with_offset.xml @@ -0,0 +1,50 @@ +<!-- + ~ SonarQube, open source software quality management tool. + ~ Copyright (C) 2008-2016 SonarSource + ~ mailto:contact AT sonarsource DOT com + ~ + ~ SonarQube is free software; you can redistribute it and/or + ~ modify it under the terms of the GNU Lesser General Public + ~ License as published by the Free Software Foundation; either + ~ version 3 of the License, or (at your option) any later version. + ~ + ~ SonarQube is distributed in the hope that it will be useful, + ~ but WITHOUT ANY WARRANTY; without even the implied warranty of + ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + ~ Lesser General Public License for more details. + ~ + ~ You should have received a copy of the GNU Lesser General Public License + ~ along with this program; if not, write to the Free Software Foundation, + ~ Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + --> + +<sqale> + <chc> + <key>USABILITY</key> + <name>Usability</name> + <desc>Estimate usability</desc> + </chc> + <chc> + <key>EFFICIENCY</key> + <name>Efficiency</name> + <chc> + <key>MEMORY_EFFICIENCY</key> + <name>Memory use</name> + <chc> + <rule-repo>checkstyle</rule-repo> + <rule-key>Regexp</rule-key> + <prop> + <key>remediationFunction</key> + <txt>constant_issue</txt> + </prop> + <prop> + <!-- Should be replaced by offset --> + <key>remediationFactor</key> + <val>3.0</val> + <txt>h</txt> + </prop> + </chc> + </chc> + </chc> + +</sqale> diff --git a/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/convert_deprecated_linear_with_threshold_function_by_linear_function.xml b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/convert_deprecated_linear_with_threshold_function_by_linear_function.xml new file mode 100644 index 00000000000..9ebc69b94a6 --- /dev/null +++ b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/convert_deprecated_linear_with_threshold_function_by_linear_function.xml @@ -0,0 +1,36 @@ +<sqale> + <chc> + <key>USABILITY</key> + <name>Usability</name> + <desc>Estimate usability</desc> + </chc> + <chc> + <key>EFFICIENCY</key> + <name>Efficiency</name> + <chc> + <key>MEMORY_EFFICIENCY</key> + <name>Memory use</name> + <chc> + <rule-repo>checkstyle</rule-repo> + <rule-key>Regexp</rule-key> + <prop> + <key>remediationFunction</key> + <!-- Should be replaced by linear --> + <txt>linear_threshold</txt> + </prop> + <prop> + <key>remediationFactor</key> + <val>3.0</val> + <txt>h</txt> + </prop> + <!-- Should be ignored --> + <prop> + <key>offset</key> + <val>1.0</val> + <txt>h</txt> + </prop> + </chc> + </chc> + </chc> + +</sqale> diff --git a/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/convert_network_use_key.xml b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/convert_network_use_key.xml new file mode 100644 index 00000000000..d4707ba62a8 --- /dev/null +++ b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/convert_network_use_key.xml @@ -0,0 +1,23 @@ +<sqale> + <chc> + <key>EFFICIENCY</key> + <name>Efficiency</name> + <chc> + <key>NETWORK_USE_EFFICIENCY</key> + <name>Network use</name> + <chc> + <rule-repo>checkstyle</rule-repo> + <rule-key>Regexp</rule-key> + <prop> + <key>remediationFactor</key> + <val>3.0</val> + <txt>h</txt> + </prop> + <prop> + <key>remediationFunction</key> + <txt>LINEAR</txt> + </prop> + </chc> + </chc> + </chc> +</sqale> diff --git a/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/fail_on_bad_xml.xml b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/fail_on_bad_xml.xml new file mode 100644 index 00000000000..3b15eae11d6 --- /dev/null +++ b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/fail_on_bad_xml.xml @@ -0,0 +1 @@ +Not a valid xml diff --git a/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/ignore_deprecated_constant_per_file_function.xml b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/ignore_deprecated_constant_per_file_function.xml new file mode 100644 index 00000000000..4b8ae3f6475 --- /dev/null +++ b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/ignore_deprecated_constant_per_file_function.xml @@ -0,0 +1,25 @@ +<sqale> + <chc> + <key>EFFICIENCY</key> + <name>Efficiency</name> + <chc> + <key>MEMORY_EFFICIENCY</key> + <name>Memory use</name> + <!-- Should be ignored --> + <chc> + <rule-repo>checkstyle</rule-repo> + <rule-key>Regexp</rule-key> + <prop> + <key>remediationFactor</key> + <val>3.0</val> + <txt>h</txt> + </prop> + <prop> + <key>remediationFunction</key> + <txt>constant_resource</txt> + </prop> + </chc> + </chc> + </chc> + +</sqale> diff --git a/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/ignore_invalid_value.xml b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/ignore_invalid_value.xml new file mode 100644 index 00000000000..bb6bdbb4afb --- /dev/null +++ b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/ignore_invalid_value.xml @@ -0,0 +1,28 @@ +<sqale> + <chc> + <key>USABILITY</key> + <name>Usability</name> + <desc>Estimate usability</desc> + </chc> + <chc> + <key>EFFICIENCY</key> + <name>Efficiency</name> + <chc> + <key>MEMORY_EFFICIENCY</key> + <name>Memory use</name> + <chc> + <rule-repo>checkstyle</rule-repo> + <rule-key>Regexp</rule-key> + <prop> + <key>factor</key> + <val>abc</val> + </prop> + <prop> + <key>function</key> + <txt>linear</txt> + </prop> + </chc> + </chc> + </chc> + +</sqale> diff --git a/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/ignore_remediation_cost_having_zero_value.xml b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/ignore_remediation_cost_having_zero_value.xml new file mode 100644 index 00000000000..2165a603341 --- /dev/null +++ b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/ignore_remediation_cost_having_zero_value.xml @@ -0,0 +1,71 @@ +<sqale> + <chc> + <key>USABILITY</key> + <name>Usability</name> + <desc>Estimate usability</desc> + <chc> + <rule-repo>squid</rule-repo> + <rule-key>S001</rule-key> + <prop> + <key>offset</key> + <val>0.0</val> + <txt>min</txt> + </prop> + <prop> + <key>remediationFunction</key> + <txt>constant_issue</txt> + </prop> + </chc> + </chc> + <chc> + <key>EFFICIENCY</key> + <name>Efficiency</name> + <chc> + <key>MEMORY_EFFICIENCY</key> + <name>Memory use</name> + <chc> + <rule-repo>checkstyle</rule-repo> + <rule-key>Regexp</rule-key> + <prop> + <key>remediationFactor</key> + <val>0.0</val> + <txt>h</txt> + </prop> + <prop> + <key>remediationFunction</key> + <txt>linear</txt> + </prop> + </chc> + </chc> + </chc> + <chc> + <key>PORTABILITY</key> + <name>Portability</name> + <chc> + <key>COMPILER_RELATED_PORTABILITY</key> + <name>Compiler related portability</name> + </chc> + <chc> + <key>HARDWARE_RELATED_PORTABILITY</key> + <name>Hardware related portability</name> + <chc> + <rule-repo>checkstyle</rule-repo> + <rule-key>Regexp2</rule-key> + <prop> + <key>remediationFunction</key> + <txt>linear_offset</txt> + </prop> + <prop> + <key>remediationFactor</key> + <val>0.0</val> + <txt>d</txt> + </prop> + <prop> + <key>offset</key> + <val>0.0</val> + <txt>d</txt> + </prop> + </chc> + </chc> + </chc> +</sqale> diff --git a/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/ignore_rule_on_root_characteristics.xml b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/ignore_rule_on_root_characteristics.xml new file mode 100644 index 00000000000..bcf3ed867d3 --- /dev/null +++ b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/ignore_rule_on_root_characteristics.xml @@ -0,0 +1,19 @@ +<sqale> + <chc> + <key>EFFICIENCY</key> + <name>Efficiency</name> + <chc> + <rule-repo>checkstyle</rule-repo> + <rule-key>Regexp</rule-key> + <prop> + <key>remediationFactor</key> + <val>3.0</val> + <txt>h</txt> + </prop> + <prop> + <key>remediationFunction</key> + <txt>linear</txt> + </prop> + </chc> + </chc> +</sqale> diff --git a/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/import_badly_formatted_xml.xml b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/import_badly_formatted_xml.xml new file mode 100644 index 00000000000..6c7d153992c --- /dev/null +++ b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/import_badly_formatted_xml.xml @@ -0,0 +1,43 @@ +<sqale> + <chc> + <key>USABILITY + </key> + <name>Usability + </name> + <desc>Estimate usability + </desc> + </chc> + <chc> + <key>EFFICIENCY + </key> + <name>Efficiency + </name> + <chc> + <key>MEMORY_EFFICIENCY + </key> + <name>Memory use + </name> + <chc> + <rule-repo>checkstyle + </rule-repo> + <rule-key>Regexp + </rule-key> + <prop> + <key>remediationFactor + </key> + <val>3.0 + </val> + <txt>h + </txt> + </prop> + <prop> + <key>remediationFunction + </key> + <txt>linear + </txt> + </prop> + </chc> + </chc> + </chc> + +</sqale> diff --git a/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/import_constant_issue.xml b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/import_constant_issue.xml new file mode 100644 index 00000000000..86b1f551fbe --- /dev/null +++ b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/import_constant_issue.xml @@ -0,0 +1,29 @@ +<sqale> + <chc> + <key>USABILITY</key> + <name>Usability</name> + <desc>Estimate usability</desc> + </chc> + <chc> + <key>EFFICIENCY</key> + <name>Efficiency</name> + <chc> + <key>MEMORY_EFFICIENCY</key> + <name>Memory use</name> + <chc> + <rule-repo>checkstyle</rule-repo> + <rule-key>Regexp</rule-key> + <prop> + <key>offset</key> + <val>3.0</val> + <txt>d</txt> + </prop> + <prop> + <key>remediationFunction</key> + <txt>constant_issue</txt> + </prop> + </chc> + </chc> + </chc> + +</sqale> diff --git a/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/import_linear.xml b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/import_linear.xml new file mode 100644 index 00000000000..f641a5185ec --- /dev/null +++ b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/import_linear.xml @@ -0,0 +1,29 @@ +<sqale> + <chc> + <key>USABILITY</key> + <name>Usability</name> + <desc>Estimate usability</desc> + </chc> + <chc> + <key>EFFICIENCY</key> + <name>Efficiency</name> + <chc> + <key>MEMORY_EFFICIENCY</key> + <name>Memory use</name> + <chc> + <rule-repo>checkstyle</rule-repo> + <rule-key>Regexp</rule-key> + <prop> + <key>remediationFactor</key> + <val>3.0</val> + <txt>h</txt> + </prop> + <prop> + <key>remediationFunction</key> + <txt>linear</txt> + </prop> + </chc> + </chc> + </chc> + +</sqale> diff --git a/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/import_linear_having_offset_to_zero.xml b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/import_linear_having_offset_to_zero.xml new file mode 100644 index 00000000000..12328da7c83 --- /dev/null +++ b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/import_linear_having_offset_to_zero.xml @@ -0,0 +1,34 @@ +<sqale> + <chc> + <key>USABILITY</key> + <name>Usability</name> + <desc>Estimate usability</desc> + </chc> + <chc> + <key>EFFICIENCY</key> + <name>Efficiency</name> + <chc> + <key>MEMORY_EFFICIENCY</key> + <name>Memory use</name> + <chc> + <rule-repo>checkstyle</rule-repo> + <rule-key>Regexp</rule-key> + <prop> + <key>remediationFactor</key> + <val>3.0</val> + <txt>h</txt> + </prop> + <prop> + <key>remediationFunction</key> + <txt>linear</txt> + </prop> + <prop> + <key>offset</key> + <val>0.0</val> + <txt>min</txt> + </prop> + </chc> + </chc> + </chc> + +</sqale> diff --git a/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/import_linear_with_offset.xml b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/import_linear_with_offset.xml new file mode 100644 index 00000000000..be4fdd3ce4e --- /dev/null +++ b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/import_linear_with_offset.xml @@ -0,0 +1,54 @@ +<!-- + ~ SonarQube, open source software quality management tool. + ~ Copyright (C) 2008-2016 SonarSource + ~ mailto:contact AT sonarsource DOT com + ~ + ~ SonarQube is free software; you can redistribute it and/or + ~ modify it under the terms of the GNU Lesser General Public + ~ License as published by the Free Software Foundation; either + ~ version 3 of the License, or (at your option) any later version. + ~ + ~ SonarQube is distributed in the hope that it will be useful, + ~ but WITHOUT ANY WARRANTY; without even the implied warranty of + ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + ~ Lesser General Public License for more details. + ~ + ~ You should have received a copy of the GNU Lesser General Public License + ~ along with this program; if not, write to the Free Software Foundation, + ~ Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + --> + +<sqale> + <chc> + <key>USABILITY</key> + <name>Usability</name> + <desc>Estimate usability</desc> + </chc> + <chc> + <key>EFFICIENCY</key> + <name>Efficiency</name> + <chc> + <key>MEMORY_EFFICIENCY</key> + <name>Memory use</name> + <chc> + <rule-repo>checkstyle</rule-repo> + <rule-key>Regexp</rule-key> + <prop> + <key>remediationFactor</key> + <val>3.0</val> + <txt>h</txt> + </prop> + <prop> + <key>remediationFunction</key> + <txt>linear_offset</txt> + </prop> + <prop> + <key>offset</key> + <val>1.0</val> + <txt>min</txt> + </prop> + </chc> + </chc> + </chc> + +</sqale> diff --git a/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/import_rules.xml b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/import_rules.xml new file mode 100644 index 00000000000..af3b906764e --- /dev/null +++ b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/import_rules.xml @@ -0,0 +1,33 @@ +<sqale> + <chc> + <rule-repo>javasquid</rule-repo> + <rule-key>rule1</rule-key> + <prop> + <key>remediationFactor</key> + <val>3.0</val> + <txt>h</txt> + </prop> + <prop> + <key>remediationFunction</key> + <txt>linear</txt> + </prop> + </chc> + <chc> + <rule-repo>javasquid</rule-repo> + <rule-key>rule2</rule-key> + <prop> + <key>remediationFactor</key> + <val>3.0</val> + <txt>h</txt> + </prop> + <prop> + <key>remediationFunction</key> + <txt>linear_offset</txt> + </prop> + <prop> + <key>offset</key> + <val>1.0</val> + <txt>h</txt> + </prop> + </chc> +</sqale> diff --git a/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/import_rules_with_deprecated_quality_model_format.xml b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/import_rules_with_deprecated_quality_model_format.xml new file mode 100644 index 00000000000..f8f7e9b6d5d --- /dev/null +++ b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/import_rules_with_deprecated_quality_model_format.xml @@ -0,0 +1,58 @@ +<sqale> + <chc> + <key>USABILITY</key> + <name>Usability</name> + <desc>Estimate usability</desc> + </chc> + <chc> + <key>EFFICIENCY</key> + <name>Efficiency</name> + <chc> + <key>MEMORY_EFFICIENCY</key> + <name>Memory use</name> + <chc> + <rule-repo>javasquid</rule-repo> + <rule-key>rule1</rule-key> + <prop> + <key>remediationFactor</key> + <val>3.0</val> + <txt>h</txt> + </prop> + <prop> + <key>remediationFunction</key> + <txt>linear</txt> + </prop> + </chc> + </chc> + </chc> + <chc> + <key>PORTABILITY</key> + <name>Portability</name> + <chc> + <key>COMPILER_RELATED_PORTABILITY</key> + <name>Compiler related portability</name> + </chc> + <chc> + <key>HARDWARE_RELATED_PORTABILITY</key> + <name>Hardware related portability</name> + <chc> + <rule-repo>javasquid</rule-repo> + <rule-key>rule2</rule-key> + <prop> + <key>remediationFactor</key> + <val>3.0</val> + <txt>h</txt> + </prop> + <prop> + <key>remediationFunction</key> + <txt>linear_offset</txt> + </prop> + <prop> + <key>offset</key> + <val>1.0</val> + <txt>h</txt> + </prop> + </chc> + </chc> + </chc> +</sqale> diff --git a/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/read_integer.xml b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/read_integer.xml new file mode 100644 index 00000000000..483a98bebd3 --- /dev/null +++ b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/read_integer.xml @@ -0,0 +1,29 @@ +<sqale> + <chc> + <key>USABILITY</key> + <name>Usability</name> + <desc>Estimate usability</desc> + </chc> + <chc> + <key>EFFICIENCY</key> + <name>Efficiency</name> + <chc> + <key>MEMORY_EFFICIENCY</key> + <name>Memory use</name> + <chc> + <rule-repo>checkstyle</rule-repo> + <rule-key>Regexp</rule-key> + <prop> + <key>remediationFactor</key> + <val>3</val> + <txt>h</txt> + </prop> + <prop> + <key>remediationFunction</key> + <txt>linear</txt> + </prop> + </chc> + </chc> + </chc> + +</sqale> diff --git a/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/replace_mn_by_min.xml b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/replace_mn_by_min.xml new file mode 100644 index 00000000000..f6b3f2dbb96 --- /dev/null +++ b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/replace_mn_by_min.xml @@ -0,0 +1,49 @@ +<!-- + ~ SonarQube, open source software quality management tool. + ~ Copyright (C) 2008-2016 SonarSource + ~ mailto:contact AT sonarsource DOT com + ~ + ~ SonarQube is free software; you can redistribute it and/or + ~ modify it under the terms of the GNU Lesser General Public + ~ License as published by the Free Software Foundation; either + ~ version 3 of the License, or (at your option) any later version. + ~ + ~ SonarQube is distributed in the hope that it will be useful, + ~ but WITHOUT ANY WARRANTY; without even the implied warranty of + ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + ~ Lesser General Public License for more details. + ~ + ~ You should have received a copy of the GNU Lesser General Public License + ~ along with this program; if not, write to the Free Software Foundation, + ~ Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + --> + +<sqale> + <chc> + <key>USABILITY</key> + <name>Usability</name> + <desc>Estimate usability</desc> + </chc> + <chc> + <key>EFFICIENCY</key> + <name>Efficiency</name> + <chc> + <key>MEMORY_EFFICIENCY</key> + <name>Memory use</name> + <chc> + <rule-repo>checkstyle</rule-repo> + <rule-key>Regexp</rule-key> + <prop> + <key>remediationFactor</key> + <val>3.0</val> + <txt>mn</txt> + </prop> + <prop> + <key>remediationFunction</key> + <txt>linear</txt> + </prop> + </chc> + </chc> + </chc> + +</sqale> diff --git a/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/use_default_unit_when_no_unit.xml b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/use_default_unit_when_no_unit.xml new file mode 100644 index 00000000000..34c2f2cd9e5 --- /dev/null +++ b/server/sonar-webserver-core/src/test/resources/org/sonar/server/debt/DebtRulesXMLImporterTest/use_default_unit_when_no_unit.xml @@ -0,0 +1,32 @@ +<sqale> + <chc> + <key>USABILITY</key> + <name>Usability</name> + <desc>Estimate usability</desc> + </chc> + <chc> + <key>EFFICIENCY</key> + <name>Efficiency</name> + <chc> + <key>MEMORY_EFFICIENCY</key> + <name>Memory use</name> + <chc> + <rule-repo>checkstyle</rule-repo> + <rule-key>Regexp</rule-key> + <prop> + <key>remediationFactor</key> + <val>3.0</val> + </prop> + <prop> + <key>remediationFunction</key> + <txt>linear_offset</txt> + </prop> + <prop> + <key>offset</key> + <val>1.0</val> + </prop> + </chc> + </chc> + </chc> + +</sqale> diff --git a/server/sonar-webserver-core/src/test/resources/org/sonar/server/metric/DefaultMetricFinderTest/shared.xml b/server/sonar-webserver-core/src/test/resources/org/sonar/server/metric/DefaultMetricFinderTest/shared.xml new file mode 100644 index 00000000000..60a47d06c7f --- /dev/null +++ b/server/sonar-webserver-core/src/test/resources/org/sonar/server/metric/DefaultMetricFinderTest/shared.xml @@ -0,0 +1,12 @@ +<dataset> + + <metrics delete_historical_data="[null]" id="1" name="ncloc" VAL_TYPE="INT" DESCRIPTION="[null]" domain="[null]" short_name="" + enabled="[true]" worst_value="[null]" optimized_best_value="[null]" best_value="[null]" direction="0" + hidden="[false]"/> + + <metrics delete_historical_data="[null]" id="2" name="coverage" VAL_TYPE="INT" DESCRIPTION="[null]" domain="[null]" short_name="" + enabled="[true]" worst_value="0" optimized_best_value="[true]" best_value="100" direction="1" hidden="[false]"/> + + <metrics delete_historical_data="[null]" id="3" name="disabled" VAL_TYPE="INT" DESCRIPTION="[null]" domain="[null]" short_name="" + enabled="[false]" worst_value="0" optimized_best_value="[true]" best_value="100" direction="1" hidden="[false]"/> +</dataset> diff --git a/server/sonar-webserver-core/src/test/resources/org/sonar/server/platform/ClassLoaderUtilsTest/ClassLoaderUtilsTest.jar b/server/sonar-webserver-core/src/test/resources/org/sonar/server/platform/ClassLoaderUtilsTest/ClassLoaderUtilsTest.jar Binary files differnew file mode 100644 index 00000000000..21024e33b94 --- /dev/null +++ b/server/sonar-webserver-core/src/test/resources/org/sonar/server/platform/ClassLoaderUtilsTest/ClassLoaderUtilsTest.jar diff --git a/server/sonar-webserver-core/src/test/resources/org/sonar/server/platform/ServerImplTest/build.properties b/server/sonar-webserver-core/src/test/resources/org/sonar/server/platform/ServerImplTest/build.properties new file mode 100644 index 00000000000..230f3ae8907 --- /dev/null +++ b/server/sonar-webserver-core/src/test/resources/org/sonar/server/platform/ServerImplTest/build.properties @@ -0,0 +1,2 @@ +Implementation-Build=0b9545a8b74aca473cb776275be4dc93a327c363 +Build-Time=1342455258749
\ No newline at end of file diff --git a/server/sonar-webserver-core/src/test/resources/org/sonar/server/platform/ServerImplTest/empty-version.txt b/server/sonar-webserver-core/src/test/resources/org/sonar/server/platform/ServerImplTest/empty-version.txt new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/server/sonar-webserver-core/src/test/resources/org/sonar/server/platform/ServerImplTest/empty-version.txt diff --git a/server/sonar-webserver-core/src/test/resources/org/sonar/server/platform/ServerImplTest/version.txt b/server/sonar-webserver-core/src/test/resources/org/sonar/server/platform/ServerImplTest/version.txt new file mode 100644 index 00000000000..d3827e75a5c --- /dev/null +++ b/server/sonar-webserver-core/src/test/resources/org/sonar/server/platform/ServerImplTest/version.txt @@ -0,0 +1 @@ +1.0 diff --git a/server/sonar-webserver-core/src/test/resources/org/sonar/server/telemetry/telemetry-example.json b/server/sonar-webserver-core/src/test/resources/org/sonar/server/telemetry/telemetry-example.json new file mode 100644 index 00000000000..5eeb14d2ff0 --- /dev/null +++ b/server/sonar-webserver-core/src/test/resources/org/sonar/server/telemetry/telemetry-example.json @@ -0,0 +1,55 @@ +{ + "id": "AU-TpxcB-iU5OvuD2FL7", + "version": "7.5.4", + "edition": "developer", + "database": { + "name": "PostgreSQL", + "version": "9.6.5" + }, + "plugins": [ + { + "name": "java", + "version": "4.12.0.11033" + }, + { + "name": "scmgit", + "version": "1.2" + }, + { + "name": "other", + "version": "undefined" + } + ], + "userCount": 3, + "projectCount": 2, + "usingBranches": true, + "ncloc": 300, + "projectCountByLanguage": [ + { + "language": "java", + "count": 2 + }, + { + "language": "kotlin", + "count": 1 + }, + { + "language": "js", + "count": 1 + } + ], + "nclocByLanguage": [ + { + "language": "java", + "ncloc": 500 + }, + { + "language": "kotlin", + "ncloc": 2500 + }, + { + "language": "js", + "ncloc": 50 + } + ] +} |