From 7c58e989e8bd977b06b0e57aa7e6cf548c34e34a Mon Sep 17 00:00:00 2001 From: =?utf8?q?S=C3=A9bastien=20Lesaint?= Date: Thu, 16 Mar 2017 10:28:37 +0100 Subject: [PATCH] SONAR-8888 add DefinedQProfileRepository --- .../platformlevel/PlatformLevel4.java | 2 + .../qualityprofile/DefinedQProfile.java | 125 ++++++++ .../DefinedQProfileRepository.java | 42 +++ .../DefinedQProfileRepositoryImpl.java | 200 ++++++++++++ .../RegisterQualityProfiles.java | 272 +--------------- .../OrganizationCreationImplTest.java | 2 + .../DefinedQProfileRepositoryImplTest.java | 291 ++++++++++++++++++ .../DefinedQProfileRepositoryRule.java | 94 ++++++ .../QProfileResetMediumTest.java | 2 +- .../qualityprofile/QualityProfileTest.java | 1 - .../RegisterQualityProfilesTest.java | 280 +++-------------- .../server/rule/RegisterRulesMediumTest.java | 2 +- 12 files changed, 813 insertions(+), 500 deletions(-) create mode 100644 server/sonar-server/src/main/java/org/sonar/server/qualityprofile/DefinedQProfile.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/qualityprofile/DefinedQProfileRepository.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/qualityprofile/DefinedQProfileRepositoryImpl.java create mode 100644 server/sonar-server/src/test/java/org/sonar/server/qualityprofile/DefinedQProfileRepositoryImplTest.java create mode 100644 server/sonar-server/src/test/java/org/sonar/server/qualityprofile/DefinedQProfileRepositoryRule.java diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java index 2a2ac3cc83f..df4b1e4cb7c 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java @@ -140,6 +140,7 @@ import org.sonar.server.projecttag.ws.ProjectTagsWsModule; import org.sonar.server.property.InternalPropertiesImpl; import org.sonar.server.property.ws.PropertiesWs; import org.sonar.server.qualitygate.QualityGateModule; +import org.sonar.server.qualityprofile.DefinedQProfileRepositoryImpl; import org.sonar.server.qualityprofile.QProfileBackuperImpl; import org.sonar.server.qualityprofile.QProfileComparison; import org.sonar.server.qualityprofile.QProfileCopier; @@ -256,6 +257,7 @@ public class PlatformLevel4 extends PlatformLevel { OrganizationsWsModule.class, // quality profile + DefinedQProfileRepositoryImpl.class, ActiveRuleIndexer.class, XMLProfileParser.class, XMLProfileSerializer.class, diff --git a/server/sonar-server/src/main/java/org/sonar/server/qualityprofile/DefinedQProfile.java b/server/sonar-server/src/main/java/org/sonar/server/qualityprofile/DefinedQProfile.java new file mode 100644 index 00000000000..278caad1a8e --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/qualityprofile/DefinedQProfile.java @@ -0,0 +1,125 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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.qualityprofile; + +import com.google.common.collect.ImmutableList; +import java.security.MessageDigest; +import java.util.ArrayList; +import java.util.List; +import javax.annotation.concurrent.Immutable; +import org.sonar.api.profiles.ProfileDefinition; + +import static java.lang.String.format; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.apache.commons.codec.binary.Hex.encodeHexString; +import static org.apache.commons.lang.StringUtils.lowerCase; +import static org.sonar.db.loadedtemplate.LoadedTemplateDto.QUALITY_PROFILE_TYPE; + +/** + * Represent a Quality Profile as computed from {@link ProfileDefinition} provided by installed plugins. + */ +@Immutable +public final class DefinedQProfile { + private final QProfileName qProfileName; + private final boolean isDefault; + private final String loadedTemplateType; + private final List activeRules; + + private DefinedQProfile(Builder builder, MessageDigest messageDigest) { + this.qProfileName = new QProfileName(builder.language, builder.getName()); + this.isDefault = builder.declaredDefault || builder.computedDefault; + this.loadedTemplateType = computeLoadedTemplateType(this.qProfileName, messageDigest); + this.activeRules = ImmutableList.copyOf(builder.activeRules); + } + + private static String computeLoadedTemplateType(QProfileName qProfileName, MessageDigest messageDigest) { + String qpIdentifier = lowerCase(qProfileName.getLanguage()) + ":" + qProfileName.getName(); + return format("%s.%s", QUALITY_PROFILE_TYPE, encodeHexString(messageDigest.digest(qpIdentifier.getBytes(UTF_8)))); + } + + public String getName() { + return qProfileName.getName(); + } + + public String getLanguage() { + return qProfileName.getLanguage(); + } + + public QProfileName getQProfileName() { + return qProfileName; + } + + public boolean isDefault() { + return isDefault; + } + + public String getLoadedTemplateType() { + return loadedTemplateType; + } + + public List getActiveRules() { + return activeRules; + } + + static final class Builder { + private String language; + private String name; + private boolean declaredDefault; + private boolean computedDefault; + private final List activeRules = new ArrayList<>(); + + public Builder setLanguage(String language) { + this.language = language; + return this; + } + + Builder setName(String name) { + this.name = name; + return this; + } + + String getName() { + return name; + } + + Builder setDeclaredDefault(boolean declaredDefault) { + this.declaredDefault = declaredDefault; + return this; + } + + boolean isDeclaredDefault() { + return declaredDefault; + } + + Builder setComputedDefault(boolean flag) { + computedDefault = flag; + return this; + } + + Builder addRules(List rules) { + this.activeRules.addAll(rules); + return this; + } + + DefinedQProfile build(MessageDigest messageDigest) { + return new DefinedQProfile(this, messageDigest); + } + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/qualityprofile/DefinedQProfileRepository.java b/server/sonar-server/src/main/java/org/sonar/server/qualityprofile/DefinedQProfileRepository.java new file mode 100644 index 00000000000..53452d1d857 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/qualityprofile/DefinedQProfileRepository.java @@ -0,0 +1,42 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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.qualityprofile; + +import java.util.List; +import java.util.Map; + +public interface DefinedQProfileRepository { + /** + * Initializes the Repository. + * + * This method is intended to be called from a startup task + * (see {@link org.sonar.server.platform.platformlevel.PlatformLevelStartup}). + * + * @throws IllegalStateException if called more then once + */ + void initialize(); + + /** + * @return an immutable map containing immutable lists. + * + * @throws IllegalStateException if {@link #initialize()} has not been called + */ + Map> getQProfilesByLanguage(); +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/qualityprofile/DefinedQProfileRepositoryImpl.java b/server/sonar-server/src/main/java/org/sonar/server/qualityprofile/DefinedQProfileRepositoryImpl.java new file mode 100644 index 00000000000..e89d2fd1e0d --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/qualityprofile/DefinedQProfileRepositoryImpl.java @@ -0,0 +1,200 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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.qualityprofile; + +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ListMultimap; +import com.google.common.collect.Multimaps; +import java.security.MessageDigest; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import javax.annotation.Nullable; +import org.apache.commons.codec.digest.DigestUtils; +import org.sonar.api.profiles.ProfileDefinition; +import org.sonar.api.profiles.RulesProfile; +import org.sonar.api.resources.Languages; +import org.sonar.api.utils.ValidationMessages; +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.util.stream.Collectors; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; +import static org.apache.commons.lang.StringUtils.isNotEmpty; +import static org.apache.commons.lang.StringUtils.lowerCase; + +public class DefinedQProfileRepositoryImpl implements DefinedQProfileRepository { + private static final Logger LOGGER = Loggers.get(DefinedQProfileRepositoryImpl.class); + private static final String DEFAULT_PROFILE_NAME = "Sonar way"; + + private final Languages languages; + private final List definitions; + private Map> qProfilesByLanguage; + + /** + * Requires for pico container when no {@link ProfileDefinition} is defined at all + */ + public DefinedQProfileRepositoryImpl(Languages languages) { + this(languages, new ProfileDefinition[0]); + } + + public DefinedQProfileRepositoryImpl(Languages languages, ProfileDefinition... definitions) { + this.languages = languages; + this.definitions = ImmutableList.copyOf(definitions); + } + + @Override + public void initialize() { + checkState(qProfilesByLanguage == null, "initialize must be called only once"); + + Profiler profiler = Profiler.create(Loggers.get(getClass())).startInfo("Load quality profiles"); + ListMultimap rulesProfilesByLanguage = buildRulesProfilesByLanguage(); + validateAndClean(rulesProfilesByLanguage); + this.qProfilesByLanguage = toQualityProfilesByLanguage(rulesProfilesByLanguage); + profiler.stopDebug(); + } + + @Override + public Map> getQProfilesByLanguage() { + checkState(qProfilesByLanguage != null, "initialize must be called first"); + + return qProfilesByLanguage; + } + + /** + * @return profiles by language + */ + private ListMultimap buildRulesProfilesByLanguage() { + ListMultimap byLang = ArrayListMultimap.create(); + for (ProfileDefinition definition : definitions) { + ValidationMessages validation = ValidationMessages.create(); + RulesProfile profile = definition.createProfile(validation); + validation.log(LOGGER); + if (profile != null && !validation.hasErrors()) { + checkArgument(isNotEmpty(profile.getName()), "Profile created by Definition %s can't have a blank name", definition); + byLang.put(lowerCase(profile.getLanguage(), Locale.ENGLISH), profile); + } + } + return byLang; + } + + private void validateAndClean(ListMultimap byLang) { + byLang.asMap().entrySet() + .removeIf(entry -> { + String language = entry.getKey(); + if (languages.get(language) == null) { + LOGGER.info("Language {} is not installed, related Quality profiles are ignored", language); + return true; + } + Collection profiles = entry.getValue(); + if (profiles.isEmpty()) { + LOGGER.warn("No Quality profiles defined for language: {}", language); + return true; + } + return false; + }); + } + + private Map> toQualityProfilesByLanguage(ListMultimap rulesProfilesByLanguage) { + Map> buildersByLanguage = Multimaps.asMap(rulesProfilesByLanguage) + .entrySet() + .stream() + .collect(Collectors.uniqueIndex(Map.Entry::getKey, DefinedQProfileRepositoryImpl::toQualityProfileBuilders)); + return buildersByLanguage + .entrySet() + .stream() + .filter(DefinedQProfileRepositoryImpl::ensureAtMostOneDeclaredDefault) + .collect(Collectors.uniqueIndex(Map.Entry::getKey, entry -> toQualityProfiles(entry.getValue()), buildersByLanguage.size())); + } + + /** + * Creates {@link DefinedQProfile.Builder} for each unique quality profile name for a given language. + * Builders will have the following properties populated: + *
    + *
  • {@link DefinedQProfile.Builder#language language}: key of the method's parameter
  • + *
  • {@link DefinedQProfile.Builder#name name}: {@link RulesProfile#getName()}
  • + *
  • {@link DefinedQProfile.Builder#declaredDefault declaredDefault}: {@code true} if at least one RulesProfile + * with a given name has {@link RulesProfile#getDefaultProfile()} is {@code true}
  • + *
  • {@link DefinedQProfile.Builder#activeRules activeRules}: the concatenate of the active rules of all + * RulesProfile with a given name
  • + *
+ */ + private static List toQualityProfileBuilders(Map.Entry> rulesProfilesByLanguage) { + String language = rulesProfilesByLanguage.getKey(); + // use a LinkedHashMap to keep order of insertion of RulesProfiles + Map qualityProfileBuildersByName = new LinkedHashMap<>(); + for (RulesProfile rulesProfile : rulesProfilesByLanguage.getValue()) { + qualityProfileBuildersByName.compute( + rulesProfile.getName(), + (name, existingBuilder) -> updateOrCreateBuilder(language, existingBuilder, rulesProfile, name)); + } + return ImmutableList.copyOf(qualityProfileBuildersByName.values()); + } + + /** + * Fails if more than one {@link DefinedQProfile.Builder#declaredDefault} is {@code true}, otherwise returns {@code true}. + */ + private static boolean ensureAtMostOneDeclaredDefault(Map.Entry> entry) { + Set declaredDefaultProfileNames = entry.getValue().stream() + .filter(DefinedQProfile.Builder::isDeclaredDefault) + .map(DefinedQProfile.Builder::getName) + .collect(Collectors.toSet()); + checkState(declaredDefaultProfileNames.size() <= 1, "Several Quality profiles are flagged as default for the language %s: %s", entry.getKey(), declaredDefaultProfileNames); + return true; + } + + private static DefinedQProfile.Builder updateOrCreateBuilder(String language, @Nullable DefinedQProfile.Builder existingBuilder, RulesProfile rulesProfile, String name) { + DefinedQProfile.Builder builder = existingBuilder; + if (builder == null) { + builder = new DefinedQProfile.Builder() + .setLanguage(language) + .setName(name); + } + Boolean defaultProfile = rulesProfile.getDefaultProfile(); + boolean declaredDefault = defaultProfile != null && defaultProfile; + return builder + // if there is multiple RulesProfiles with the same name, if at least one is declared default, + // then QualityProfile is flagged as declared default + .setDeclaredDefault(builder.isDeclaredDefault() || declaredDefault) + .addRules(rulesProfile.getActiveRules()); + } + + private static List toQualityProfiles(List builders) { + if (builders.stream().noneMatch(DefinedQProfile.Builder::isDeclaredDefault)) { + Optional sonarWayProfile = builders.stream().filter(builder -> builder.getName().equals(DEFAULT_PROFILE_NAME)).findFirst(); + if (sonarWayProfile.isPresent()) { + sonarWayProfile.get().setComputedDefault(true); + } else { + builders.iterator().next().setComputedDefault(true); + } + } + MessageDigest md5Digest = DigestUtils.getMd5Digest(); + return builders.stream() + .map(builder -> builder.build(md5Digest)) + .collect(Collectors.toList(builders.size())); + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/qualityprofile/RegisterQualityProfiles.java b/server/sonar-server/src/main/java/org/sonar/server/qualityprofile/RegisterQualityProfiles.java index df177787b6a..23ef3093c42 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/qualityprofile/RegisterQualityProfiles.java +++ b/server/sonar-server/src/main/java/org/sonar/server/qualityprofile/RegisterQualityProfiles.java @@ -19,33 +19,14 @@ */ package org.sonar.server.qualityprofile; -import com.google.common.collect.ArrayListMultimap; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ListMultimap; -import com.google.common.collect.Multimaps; -import java.security.MessageDigest; import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import javax.annotation.Nullable; -import org.apache.commons.codec.digest.DigestUtils; -import org.sonar.api.profiles.ProfileDefinition; -import org.sonar.api.profiles.RulesProfile; -import org.sonar.api.resources.Languages; import org.sonar.api.rule.RuleKey; import org.sonar.api.rules.ActiveRuleParam; import org.sonar.api.server.ServerSide; -import org.sonar.api.utils.ValidationMessages; 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.util.stream.Collectors; import org.sonar.db.DbClient; import org.sonar.db.DbSession; import org.sonar.db.Pagination; @@ -54,15 +35,7 @@ import org.sonar.db.organization.OrganizationDto; import org.sonar.db.qualityprofile.QualityProfileDto; import org.sonar.server.qualityprofile.index.ActiveRuleIndexer; -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkState; -import static java.lang.String.format; -import static java.nio.charset.StandardCharsets.UTF_8; -import static org.apache.commons.codec.binary.Hex.encodeHexString; -import static org.apache.commons.lang.StringUtils.isNotEmpty; -import static org.apache.commons.lang.StringUtils.lowerCase; import static org.sonar.db.Pagination.forPage; -import static org.sonar.db.loadedtemplate.LoadedTemplateDto.QUALITY_PROFILE_TYPE; /** * Synchronize Quality profiles during server startup @@ -71,175 +44,43 @@ import static org.sonar.db.loadedtemplate.LoadedTemplateDto.QUALITY_PROFILE_TYPE public class RegisterQualityProfiles { private static final Logger LOGGER = Loggers.get(RegisterQualityProfiles.class); - private static final String DEFAULT_PROFILE_NAME = "Sonar way"; private static final Pagination PROCESSED_ORGANIZATIONS_BATCH_SIZE = forPage(1).andSize(2000); - private final List definitions; + private final DefinedQProfileRepository definedQProfileRepository; private final DbClient dbClient; private final QProfileFactory profileFactory; private final RuleActivator ruleActivator; - private final Languages languages; private final ActiveRuleIndexer activeRuleIndexer; - /** - * To be kept when no ProfileDefinition are injected - */ - public RegisterQualityProfiles(DbClient dbClient, - QProfileFactory profileFactory, CachingRuleActivator ruleActivator, Languages languages, ActiveRuleIndexer activeRuleIndexer) { - this(dbClient, profileFactory, ruleActivator, Collections.emptyList(), languages, activeRuleIndexer); - } - - public RegisterQualityProfiles(DbClient dbClient, - QProfileFactory profileFactory, CachingRuleActivator ruleActivator, - List definitions, Languages languages, ActiveRuleIndexer activeRuleIndexer) { + public RegisterQualityProfiles(DefinedQProfileRepository definedQProfileRepository, + DbClient dbClient, + QProfileFactory profileFactory, CachingRuleActivator ruleActivator, ActiveRuleIndexer activeRuleIndexer) { + this.definedQProfileRepository = definedQProfileRepository; this.dbClient = dbClient; this.profileFactory = profileFactory; this.ruleActivator = ruleActivator; - this.definitions = definitions; - this.languages = languages; this.activeRuleIndexer = activeRuleIndexer; } public void start() { Profiler profiler = Profiler.create(Loggers.get(getClass())).startInfo("Register quality profiles"); - ListMultimap rulesProfilesByLanguage = buildRulesProfilesByLanguage(); - validateAndClean(rulesProfilesByLanguage); - Map> qualityProfilesByLanguage = toQualityProfilesByLanguage(rulesProfilesByLanguage); - if (qualityProfilesByLanguage.isEmpty()) { - // do not open DB session if there is no quality profile to register - profiler.stopDebug("No quality profile to register"); - return; - } - + definedQProfileRepository.initialize(); try (DbSession session = dbClient.openSession(false)) { List changes = new ArrayList<>(); - qualityProfilesByLanguage.entrySet() + definedQProfileRepository.getQProfilesByLanguage().entrySet() .forEach(entry -> registerPerLanguage(session, entry.getValue(), changes)); activeRuleIndexer.index(changes); profiler.stopDebug(); } } - /** - * @return profiles by language - */ - private ListMultimap buildRulesProfilesByLanguage() { - ListMultimap byLang = ArrayListMultimap.create(); - for (ProfileDefinition definition : definitions) { - ValidationMessages validation = ValidationMessages.create(); - RulesProfile profile = definition.createProfile(validation); - validation.log(LOGGER); - if (profile != null && !validation.hasErrors()) { - checkArgument(isNotEmpty(profile.getName()), "Profile created by Definition %s can't have a blank name", definition); - byLang.put(lowerCase(profile.getLanguage(), Locale.ENGLISH), profile); - } - } - return byLang; - } - - private void validateAndClean(ListMultimap byLang) { - byLang.asMap().entrySet() - .removeIf(entry -> { - String language = entry.getKey(); - if (languages.get(language) == null) { - LOGGER.info("Language {} is not installed, related Quality profiles are ignored", language); - return true; - } - Collection profiles = entry.getValue(); - if (profiles.isEmpty()) { - LOGGER.warn("No Quality profiles defined for language: {}", language); - return true; - } - return false; - }); - } - - private Map> toQualityProfilesByLanguage(ListMultimap rulesProfilesByLanguage) { - Map> buildersByLanguage = Multimaps.asMap(rulesProfilesByLanguage) - .entrySet() - .stream() - .collect(Collectors.uniqueIndex(Map.Entry::getKey, RegisterQualityProfiles::toQualityProfileBuilders)); - return buildersByLanguage - .entrySet() - .stream() - .filter(RegisterQualityProfiles::ensureAtMostOneDeclaredDefault) - .collect(Collectors.uniqueIndex(Map.Entry::getKey, entry -> toQualityProfiles(entry.getValue()), buildersByLanguage.size())); - } - - /** - * Creates {@link QualityProfile.Builder} for each unique quality profile name for a given language. - * Builders will have the following properties populated: - *
    - *
  • {@link QualityProfile.Builder#language language}: key of the method's parameter
  • - *
  • {@link QualityProfile.Builder#name name}: {@link RulesProfile#getName()}
  • - *
  • {@link QualityProfile.Builder#declaredDefault declaredDefault}: {@code true} if at least one RulesProfile - * with a given name has {@link RulesProfile#getDefaultProfile()} is {@code true}
  • - *
  • {@link QualityProfile.Builder#activeRules activeRules}: the concatenate of the active rules of all - * RulesProfile with a given name
  • - *
- */ - private static List toQualityProfileBuilders(Map.Entry> rulesProfilesByLanguage) { - String language = rulesProfilesByLanguage.getKey(); - // use a LinkedHashMap to keep order of insertion of RulesProfiles - Map qualityProfileBuildersByName = new LinkedHashMap<>(); - for (RulesProfile rulesProfile : rulesProfilesByLanguage.getValue()) { - qualityProfileBuildersByName.compute( - rulesProfile.getName(), - (name, existingBuilder) -> updateOrCreateBuilder(language, existingBuilder, rulesProfile, name)); - } - return ImmutableList.copyOf(qualityProfileBuildersByName.values()); - } - - /** - * Fails if more than one {@link QualityProfile.Builder#declaredDefault} is {@code true}, otherwise returns {@code true}. - */ - private static boolean ensureAtMostOneDeclaredDefault(Map.Entry> entry) { - Set declaredDefaultProfileNames = entry.getValue().stream() - .filter(QualityProfile.Builder::isDeclaredDefault) - .map(QualityProfile.Builder::getName) - .collect(Collectors.toSet()); - checkState(declaredDefaultProfileNames.size() <= 1, "Several Quality profiles are flagged as default for the language %s: %s", entry.getKey(), declaredDefaultProfileNames); - return true; - } - - private static QualityProfile.Builder updateOrCreateBuilder(String language, @Nullable QualityProfile.Builder existingBuilder, RulesProfile rulesProfile, String name) { - QualityProfile.Builder builder = existingBuilder; - if (builder == null) { - builder = new QualityProfile.Builder() - .setLanguage(language) - .setName(name); - } - Boolean defaultProfile = rulesProfile.getDefaultProfile(); - boolean declaredDefault = defaultProfile != null && defaultProfile; - return builder - // if there is multiple RulesProfiles with the same name, if at least one is declared default, - // then QualityProfile is flagged as declared default - .setDeclaredDefault(builder.declaredDefault || declaredDefault) - .addRules(rulesProfile.getActiveRules()); - } - - private static List toQualityProfiles(List builders) { - if (builders.stream().noneMatch(QualityProfile.Builder::isDeclaredDefault)) { - Optional sonarWayProfile = builders.stream().filter(builder -> builder.getName().equals(DEFAULT_PROFILE_NAME)).findFirst(); - if (sonarWayProfile.isPresent()) { - sonarWayProfile.get().setComputedDefault(true); - } else { - builders.iterator().next().setComputedDefault(true); - } - } - MessageDigest md5Digest = DigestUtils.getMd5Digest(); - return builders.stream() - .map(builder -> builder.build(md5Digest)) - .collect(Collectors.toList(builders.size())); - } - - private void registerPerLanguage(DbSession session, List qualityProfiles, List changes) { + private void registerPerLanguage(DbSession session, List qualityProfiles, List changes) { qualityProfiles.forEach(qp -> registerPerQualityProfile(session, qp, changes)); session.commit(); } - private void registerPerQualityProfile(DbSession session, QualityProfile qualityProfile, List changes) { + private void registerPerQualityProfile(DbSession session, DefinedQProfile qualityProfile, List changes) { LOGGER.info("Register profile {}", qualityProfile.getQProfileName()); List organizationDtos; @@ -248,12 +89,12 @@ public class RegisterQualityProfiles { } } - private List getOrganizationsWithoutQP(DbSession session, QualityProfile qualityProfile) { + private List getOrganizationsWithoutQP(DbSession session, DefinedQProfile qualityProfile) { return dbClient.organizationDao().selectOrganizationsWithoutLoadedTemplate(session, qualityProfile.getLoadedTemplateType(), PROCESSED_ORGANIZATIONS_BATCH_SIZE); } - private void registerPerQualityProfileAndOrganization(DbSession session, QualityProfile qualityProfile, OrganizationDto organization, List changes) { + private void registerPerQualityProfileAndOrganization(DbSession session, DefinedQProfile qualityProfile, OrganizationDto organization, List changes) { LOGGER.debug("Register profile {} for organization {}", qualityProfile.getQProfileName(), organization.getKey()); QualityProfileDto profileDto = dbClient.qualityProfileDao().selectByNameAndLanguage(organization, qualityProfile.getName(), qualityProfile.getLanguage(), session); @@ -276,95 +117,4 @@ public class RegisterQualityProfiles { session.commit(); } - private static final class QualityProfile { - private final QProfileName qProfileName; - private final boolean isDefault; - private final String loadedTemplateType; - private final List activeRules; - - public QualityProfile(Builder builder, MessageDigest messageDigest) { - this.qProfileName = new QProfileName(builder.getLanguage(), builder.getName()); - this.isDefault = builder.declaredDefault || builder.computedDefault; - this.loadedTemplateType = computeLoadedTemplateType(this.qProfileName, messageDigest); - this.activeRules = ImmutableList.copyOf(builder.activeRules); - } - - private static String computeLoadedTemplateType(QProfileName qProfileName, MessageDigest messageDigest) { - String qpIdentifier = lowerCase(qProfileName.getLanguage(), Locale.ENGLISH) + ":" + qProfileName.getName(); - return format("%s.%s", QUALITY_PROFILE_TYPE, encodeHexString(messageDigest.digest(qpIdentifier.getBytes(UTF_8)))); - } - - public String getName() { - return qProfileName.getName(); - } - - public String getLanguage() { - return qProfileName.getLanguage(); - } - - public QProfileName getQProfileName() { - return qProfileName; - } - - public boolean isDefault() { - return isDefault; - } - - public String getLoadedTemplateType() { - return loadedTemplateType; - } - - public List getActiveRules() { - return activeRules; - } - - private static final class Builder { - private String language; - private String name; - private boolean declaredDefault; - private boolean computedDefault; - private List activeRules = new ArrayList<>(); - - public String getLanguage() { - return language; - } - - public Builder setLanguage(String language) { - this.language = language; - return this; - } - - Builder setName(String name) { - this.name = name; - return this; - } - - String getName() { - return name; - } - - Builder setDeclaredDefault(boolean declaredDefault) { - this.declaredDefault = declaredDefault; - return this; - } - - boolean isDeclaredDefault() { - return declaredDefault; - } - - Builder setComputedDefault(boolean flag) { - computedDefault = flag; - return this; - } - - Builder addRules(List rules) { - this.activeRules.addAll(rules); - return this; - } - - QualityProfile build(MessageDigest messageDigest) { - return new QualityProfile(this, messageDigest); - } - } - } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/organization/OrganizationCreationImplTest.java b/server/sonar-server/src/test/java/org/sonar/server/organization/OrganizationCreationImplTest.java index 9bf636f7572..a37dbb129bf 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/organization/OrganizationCreationImplTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/organization/OrganizationCreationImplTest.java @@ -214,6 +214,7 @@ public class OrganizationCreationImplTest { @Test public void create_add_current_user_as_member_of_organization() throws OrganizationCreation.KeyConflictException { mockForSuccessfulInsert(SOME_UUID, SOME_DATE); + definedQProfileRepositoryRule.initialize(); underTest.create(dbSession, SOME_USER_ID, FULL_POPULATED_NEW_ORGANIZATION); @@ -362,6 +363,7 @@ public class OrganizationCreationImplTest { when(organizationValidation.generateKeyFrom(A_LOGIN)).thenReturn(SLUG_OF_A_LOGIN); mockForSuccessfulInsert(SOME_UUID, SOME_DATE); enableCreatePersonalOrg(true); + definedQProfileRepositoryRule.initialize(); underTest.createForUser(dbSession, user); diff --git a/server/sonar-server/src/test/java/org/sonar/server/qualityprofile/DefinedQProfileRepositoryImplTest.java b/server/sonar-server/src/test/java/org/sonar/server/qualityprofile/DefinedQProfileRepositoryImplTest.java new file mode 100644 index 00000000000..a302f6dd78e --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/qualityprofile/DefinedQProfileRepositoryImplTest.java @@ -0,0 +1,291 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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.qualityprofile; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Random; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.profiles.ProfileDefinition; +import org.sonar.api.profiles.RulesProfile; +import org.sonar.api.resources.Language; +import org.sonar.api.resources.Languages; +import org.sonar.api.utils.ValidationMessages; +import org.sonar.server.language.LanguageTesting; + +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; + +public class DefinedQProfileRepositoryImplTest { + private static final Language FOO_LANGUAGE = LanguageTesting.newLanguage("foo", "foo", "foo"); + private static final String SONAR_WAY_QP_NAME = "Sonar way"; + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Test + public void getQProfilesByLanguage_throws_ISE_if_called_before_initialize() { + DefinedQProfileRepositoryImpl underTest = new DefinedQProfileRepositoryImpl(new Languages()); + + expectedException.expect(IllegalStateException.class); + expectedException.expectMessage("initialize must be called first"); + + underTest.getQProfilesByLanguage(); + } + + @Test + public void initialize_throws_ISE_if_called_twice() { + DefinedQProfileRepositoryImpl underTest = new DefinedQProfileRepositoryImpl(new Languages()); + underTest.initialize(); + + expectedException.expect(IllegalStateException.class); + expectedException.expectMessage("initialize must be called only once"); + + underTest.initialize(); + } + + @Test + public void initialize_creates_no_DefinedQProfile_when_there_is_no_definition() { + DefinedQProfileRepository underTest = new DefinedQProfileRepositoryImpl(new Languages(FOO_LANGUAGE)); + + underTest.initialize(); + + assertThat(underTest.getQProfilesByLanguage()).isEmpty(); + } + + @Test + public void initialize_creates_no_DefinedQProfile_when_all_definitions_apply_to_non_defined_languages() { + DefinedQProfileRepository underTest = new DefinedQProfileRepositoryImpl(new Languages(), new DummyProfileDefinition("foo", "P1", false)); + + underTest.initialize(); + + assertThat(underTest.getQProfilesByLanguage()).isEmpty(); + } + + @Test + public void initialize_throws_IAE_if_profileDefinition_creates_RulesProfile_with_null_name() { + DummyProfileDefinition definition = new DummyProfileDefinition("foo", null, false); + DefinedQProfileRepository underTest = new DefinedQProfileRepositoryImpl(new Languages(), definition); + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Profile created by Definition " + definition + " can't have a blank name"); + + underTest.initialize(); + } + + @Test + public void initialize_throws_IAE_if_profileDefinition_creates_RulesProfile_with_empty_name() { + DummyProfileDefinition definition = new DummyProfileDefinition("foo", "", false); + DefinedQProfileRepository underTest = new DefinedQProfileRepositoryImpl(new Languages(), definition); + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Profile created by Definition " + definition + " can't have a blank name"); + + underTest.initialize(); + } + + @Test + public void initialize_makes_single_qp_of_a_language_default_even_if_not_flagged_as_so() { + DefinedQProfileRepository underTest = new DefinedQProfileRepositoryImpl(new Languages(FOO_LANGUAGE), new DummyProfileDefinition("foo", "foo1", false)); + + underTest.initialize(); + + Map> qProfilesByLanguage = underTest.getQProfilesByLanguage(); + assertThat(qProfilesByLanguage) + .hasSize(1) + .containsOnlyKeys(FOO_LANGUAGE.getKey()); + assertThat(qProfilesByLanguage.get(FOO_LANGUAGE.getKey())) + .extracting(DefinedQProfile::isDefault) + .containsExactly(true); + } + + @Test + public void initialize_makes_single_qp_of_a_language_default_even_if_flagged_as_so() { + DefinedQProfileRepository underTest = new DefinedQProfileRepositoryImpl(new Languages(FOO_LANGUAGE), new DummyProfileDefinition("foo", "foo1", true)); + + underTest.initialize(); + + Map> qProfilesByLanguage = underTest.getQProfilesByLanguage(); + assertThat(qProfilesByLanguage) + .hasSize(1) + .containsOnlyKeys(FOO_LANGUAGE.getKey()); + assertThat(qProfilesByLanguage.get(FOO_LANGUAGE.getKey())) + .extracting(DefinedQProfile::isDefault) + .containsExactly(true); + } + + @Test + public void initialize_makes_first_qp_of_a_language_default_when_none_flagged_as_so() { + List definitions = new ArrayList<>( + asList(new DummyProfileDefinition("foo", "foo1", false), new DummyProfileDefinition("foo", "foo2", false))); + Collections.shuffle(definitions); + String firstQPName = definitions.get(0).getName(); + String secondQPName = definitions.get(1).getName(); + DefinedQProfileRepository underTest = new DefinedQProfileRepositoryImpl(new Languages(FOO_LANGUAGE), definitions.stream().toArray(ProfileDefinition[]::new)); + + underTest.initialize(); + + Map> qProfilesByLanguage = underTest.getQProfilesByLanguage(); + assertThat(qProfilesByLanguage) + .hasSize(1) + .containsOnlyKeys(FOO_LANGUAGE.getKey()); + List fooDefinedQProfiles = qProfilesByLanguage.get(FOO_LANGUAGE.getKey()); + assertThat(fooDefinedQProfiles) + .extracting(DefinedQProfile::getName) + .containsExactly(firstQPName, secondQPName); + assertThat(fooDefinedQProfiles) + .extracting(DefinedQProfile::isDefault) + .containsExactly(true, false); + } + + @Test + public void initialize_fails_with_ISE_when_two_sq_with_different_name_are_default_for_the_same_language() { + DefinedQProfileRepository underTest = new DefinedQProfileRepositoryImpl(new Languages(FOO_LANGUAGE), + new DummyProfileDefinition("foo", "foo1", true), new DummyProfileDefinition("foo", "foo2", true)); + + expectedException.expect(IllegalStateException.class); + expectedException.expectMessage("Several Quality profiles are flagged as default for the language foo: [foo1, foo2]"); + + underTest.initialize(); + } + + @Test + public void initialize_create_qp_as_default_even_if_only_one_profile_with_given_name_has_default_flag_true() { + String name = "doh"; + boolean flag = new Random().nextBoolean(); + DefinedQProfileRepository underTest = new DefinedQProfileRepositoryImpl(new Languages(FOO_LANGUAGE), + new DummyProfileDefinition("foo", name, flag), new DummyProfileDefinition("foo", name, !flag)); + + underTest.initialize(); + + assertThat(underTest.getQProfilesByLanguage().get(FOO_LANGUAGE.getKey())) + .extracting(DefinedQProfile::isDefault) + .containsExactly(true); + } + + @Test + public void initialize_creates_single_qp_if_several_profile_have_the_same_name_for_a_given_language() { + DefinedQProfileRepository underTest = new DefinedQProfileRepositoryImpl(new Languages(FOO_LANGUAGE), + new DummyProfileDefinition("foo", "foo1", true), new DummyProfileDefinition("foo", "foo1", true)); + + underTest.initialize(); + + Map> qProfilesByLanguage = underTest.getQProfilesByLanguage(); + assertThat(qProfilesByLanguage) + .hasSize(1) + .containsOnlyKeys(FOO_LANGUAGE.getKey()); + assertThat(qProfilesByLanguage.get(FOO_LANGUAGE.getKey())) + .extracting(DefinedQProfile::getName) + .containsExactly("foo1"); + } + + @Test + public void initialize_creates_qp_Sonar_Way_as_default_if_none_other_is_defined_default_for_a_given_language() { + DefinedQProfileRepository underTest = new DefinedQProfileRepositoryImpl( + new Languages(FOO_LANGUAGE), + new DummyProfileDefinition("foo", "doh", false), new DummyProfileDefinition("foo", "boo", false), + new DummyProfileDefinition("foo", SONAR_WAY_QP_NAME, false), new DummyProfileDefinition("foo", "goo", false)); + + underTest.initialize(); + + Map> qProfilesByLanguage = underTest.getQProfilesByLanguage(); + assertThat(qProfilesByLanguage) + .hasSize(1) + .containsOnlyKeys(FOO_LANGUAGE.getKey()); + List fooDefinedQProfiles = qProfilesByLanguage.get(FOO_LANGUAGE.getKey()); + assertThat(fooDefinedQProfiles) + .extracting(DefinedQProfile::getName) + .containsExactly("doh", "boo", SONAR_WAY_QP_NAME, "goo"); + assertThat(fooDefinedQProfiles) + .extracting(DefinedQProfile::isDefault) + .containsExactly(false, false, true, false); + } + + @Test + public void initialize_does_not_create_Sonar_Way_as_default_if_other_profile_is_defined_as_default() { + DefinedQProfileRepository underTest = new DefinedQProfileRepositoryImpl( + new Languages(FOO_LANGUAGE), + new DummyProfileDefinition("foo", SONAR_WAY_QP_NAME, false), new DummyProfileDefinition("foo", "goo", true)); + + underTest.initialize(); + + List fooDefinedQProfiles = underTest.getQProfilesByLanguage().get(FOO_LANGUAGE.getKey()); + assertThat(fooDefinedQProfiles) + .extracting(DefinedQProfile::getName) + .containsExactly(SONAR_WAY_QP_NAME, "goo"); + assertThat(fooDefinedQProfiles) + .extracting(DefinedQProfile::isDefault) + .containsExactly(false, true); + } + + @Test + public void initialize_matches_Sonar_Way_default_with_case_sensitivity() { + String sonarWayInOtherCase = SONAR_WAY_QP_NAME.toUpperCase(); + DefinedQProfileRepository underTest = new DefinedQProfileRepositoryImpl( + new Languages(FOO_LANGUAGE), + new DummyProfileDefinition("foo", "goo", false), new DummyProfileDefinition("foo", sonarWayInOtherCase, false)); + + underTest.initialize(); + + List fooDefinedQProfiles = underTest.getQProfilesByLanguage().get(FOO_LANGUAGE.getKey()); + assertThat(fooDefinedQProfiles) + .extracting(DefinedQProfile::getName) + .containsExactly("goo", sonarWayInOtherCase); + assertThat(fooDefinedQProfiles) + .extracting(DefinedQProfile::isDefault) + .containsExactly(true, false); + } + + private static final class DummyProfileDefinition extends ProfileDefinition { + private final String language; + private final String name; + private final boolean defaultProfile; + + private DummyProfileDefinition(String language, String name, boolean defaultProfile) { + this.language = language; + this.name = name; + this.defaultProfile = defaultProfile; + } + + @Override + public RulesProfile createProfile(ValidationMessages validation) { + RulesProfile res = RulesProfile.create(name, language); + res.setDefaultProfile(defaultProfile); + return res; + } + + String getLanguage() { + return language; + } + + String getName() { + return name; + } + + boolean isDefaultProfile() { + return defaultProfile; + } + } + +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/qualityprofile/DefinedQProfileRepositoryRule.java b/server/sonar-server/src/test/java/org/sonar/server/qualityprofile/DefinedQProfileRepositoryRule.java new file mode 100644 index 00000000000..b6745a0f46a --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/qualityprofile/DefinedQProfileRepositoryRule.java @@ -0,0 +1,94 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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.qualityprofile; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; +import org.apache.commons.codec.digest.DigestUtils; +import org.junit.rules.ExternalResource; +import org.sonar.api.resources.Language; +import org.sonar.core.util.stream.Collectors; + +import static com.google.common.base.Preconditions.checkState; +import static org.sonar.core.util.stream.Collectors.toList; + +public class DefinedQProfileRepositoryRule extends ExternalResource implements DefinedQProfileRepository { + private boolean initializeCalled = false; + private Map> qProfilesbyLanguage = new HashMap<>(); + + @Override + protected void before() throws Throwable { + this.initializeCalled = false; + this.qProfilesbyLanguage.clear(); + } + + @Override + public void initialize() { + checkState(!initializeCalled, "initialize must be called only once"); + this.initializeCalled = true; + } + + @Override + public Map> getQProfilesByLanguage() { + checkState(initializeCalled, "initialize must be called first"); + + return ImmutableMap.copyOf(qProfilesbyLanguage); + } + + public boolean isInitialized() { + return initializeCalled; + } + + public DefinedQProfileRepositoryRule set(String languageKey, DefinedQProfile first, DefinedQProfile... others) { + qProfilesbyLanguage.put( + languageKey, + Stream.concat(Stream.of(first), Arrays.stream(others)).collect(toList(1 + others.length))); + return this; + } + + public DefinedQProfile add(Language language, String profileName) { + return add(language, profileName, false); + } + + public DefinedQProfile add(Language language, String profileName, boolean isDefault) { + DefinedQProfile definedQProfile = create(language, profileName, isDefault); + qProfilesbyLanguage.compute(language.getKey(), + (key, existing) -> { + if (existing == null) { + return ImmutableList.of(definedQProfile); + } + return Stream.concat(existing.stream(), Stream.of(definedQProfile)).collect(Collectors.toList(existing.size() + 1)); + }); + return definedQProfile; + } + + private DefinedQProfile create(Language language, String profileName, boolean isDefault) { + return new DefinedQProfile.Builder() + .setLanguage(language.getKey()) + .setName(profileName) + .setDeclaredDefault(isDefault) + .build(DigestUtils.getMd5Digest()); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/qualityprofile/QProfileResetMediumTest.java b/server/sonar-server/src/test/java/org/sonar/server/qualityprofile/QProfileResetMediumTest.java index 1ac983d31e5..d3a7b06208d 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/qualityprofile/QProfileResetMediumTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/qualityprofile/QProfileResetMediumTest.java @@ -91,7 +91,7 @@ public class QProfileResetMediumTest { } RULE_DEFS.set(rules); PROFILE_DEFS.set(profile); - tester.get(Platform.class).executeStartupTasks(); + tester.get(Platform.class).restart(); db = tester.get(DbClient.class); dbSession = tester.get(DbClient.class).openSession(false); diff --git a/server/sonar-server/src/test/java/org/sonar/server/qualityprofile/QualityProfileTest.java b/server/sonar-server/src/test/java/org/sonar/server/qualityprofile/QualityProfileTest.java index 37e98962f39..e7f84617fb7 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/qualityprofile/QualityProfileTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/qualityprofile/QualityProfileTest.java @@ -76,6 +76,5 @@ public class QualityProfileTest { @Test public void verify_toString() { assertThat(QUALITY_PROFILE.toString()).isEqualTo("QualityProfile{key=qpKey, name=qpName, language=languageKey, rulesUpdatedAt=1274194245000}"); - } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/qualityprofile/RegisterQualityProfilesTest.java b/server/sonar-server/src/test/java/org/sonar/server/qualityprofile/RegisterQualityProfilesTest.java index 6c61a05c8a7..d25d5f8f88c 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/qualityprofile/RegisterQualityProfilesTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/qualityprofile/RegisterQualityProfilesTest.java @@ -19,21 +19,14 @@ */ package org.sonar.server.qualityprofile; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; -import java.util.List; -import org.apache.commons.codec.digest.DigestUtils; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; -import org.sonar.api.profiles.ProfileDefinition; -import org.sonar.api.profiles.RulesProfile; import org.sonar.api.resources.Language; -import org.sonar.api.resources.Languages; import org.sonar.api.utils.System2; -import org.sonar.api.utils.ValidationMessages; import org.sonar.core.util.UuidFactory; import org.sonar.db.DbClient; import org.sonar.db.DbTester; @@ -44,13 +37,10 @@ import org.sonar.server.language.LanguageTesting; import org.sonar.server.qualityprofile.index.ActiveRuleIndexer; import org.sonar.server.tester.UserSessionRule; -import static java.lang.String.format; -import static java.nio.charset.StandardCharsets.UTF_8; -import static java.util.Arrays.asList; -import static org.apache.commons.lang.StringUtils.lowerCase; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import static org.sonar.core.util.UtcDateUtils.formatDateTime; @@ -58,8 +48,6 @@ public class RegisterQualityProfilesTest { private static final Language FOO_LANGUAGE = LanguageTesting.newLanguage("foo", "foo", "foo"); private static final Language BAR_LANGUAGE = LanguageTesting.newLanguage("bar", "bar", "bar"); private static final String TABLE_RULES_PROFILES = "RULES_PROFILES"; - private static final String TYPE_QUALITY_PROFILE = "QUALITY_PROFILE"; - private static final String SONAR_WAY_QP_NAME = "Sonar way"; private static final String TABLE_LOADED_TEMPLATES = "loaded_templates"; @Rule @@ -68,65 +56,45 @@ public class RegisterQualityProfilesTest { public UserSessionRule userSessionRule = UserSessionRule.standalone(); @Rule public ExpectedException expectedException = ExpectedException.none(); + @Rule + public DefinedQProfileRepositoryRule definedQProfileRepositoryRule = new DefinedQProfileRepositoryRule(); private DbClient dbClient = dbTester.getDbClient(); private DbClient mockedDbClient = mock(DbClient.class); private UuidFactory mockedUuidFactory = mock(UuidFactory.class); private System2 mockedSystem2 = mock(System2.class); private ActiveRuleIndexer mockedActiveRuleIndexer = mock(ActiveRuleIndexer.class); + private RegisterQualityProfiles underTest = new RegisterQualityProfiles( + definedQProfileRepositoryRule, + dbClient, + new QProfileFactory(dbClient, mockedUuidFactory, mockedSystem2), + new CachingRuleActivator(mockedSystem2, dbClient, null, new CachingRuleActivatorContextFactory(dbClient), null, null, userSessionRule), + mockedActiveRuleIndexer); @Test - public void no_action_in_DB_nor_index_when_there_is_no_definition() { - RegisterQualityProfiles underTest = mockedDBAndEs(Collections.emptyList(), new Languages(FOO_LANGUAGE)); - - underTest.start(); - - verifyZeroInteractions(mockedDbClient, mockedActiveRuleIndexer); - } - - @Test - public void no_action_in_DB_nor_index_when_all_definitions_apply_to_non_defined_languages() { - RegisterQualityProfiles underTest = mockedDBAndEs(Collections.singletonList(new DummyProfileDefinition("foo", "P1", false)), new Languages()); + public void no_action_in_DB_nothing_to_index_when_there_is_no_DefinedQProfile() { + RegisterQualityProfiles underTest = new RegisterQualityProfiles(definedQProfileRepositoryRule, mockedDbClient, null, null, mockedActiveRuleIndexer); underTest.start(); - verifyZeroInteractions(mockedDbClient, mockedActiveRuleIndexer); + assertThat(definedQProfileRepositoryRule.isInitialized()).isTrue(); + verify(mockedDbClient).openSession(false); + verify(mockedActiveRuleIndexer).index(Collections.emptyList()); + verifyNoMoreInteractions(mockedDbClient, mockedActiveRuleIndexer); } @Test - public void start_throws_IAE_if_profileDefinition_creates_RulesProfile_with_null_name() { - DummyProfileDefinition definition = new DummyProfileDefinition("foo", null, false); - RegisterQualityProfiles underTest = mockedDBAndEs(Collections.singletonList(definition), new Languages()); - - expectedException.expect(IllegalArgumentException.class); - expectedException.expectMessage("Profile created by Definition " + definition + " can't have a blank name"); - - underTest.start(); - } - - @Test - public void start_throws_IAE_if_profileDefinition_creates_RulesProfile_with_empty_name() { - DummyProfileDefinition definition = new DummyProfileDefinition("foo", "", false); - RegisterQualityProfiles underTest = mockedDBAndEs(Collections.singletonList(definition), new Languages()); - - expectedException.expect(IllegalArgumentException.class); - expectedException.expectMessage("Profile created by Definition " + definition + " can't have a blank name"); - - underTest.start(); - } - - @Test - public void start_creates_qp_if_language_exists_and_store_flag_in_loaded_templates_for_each_organization_id_BD() { + public void start_creates_qp_and_store_flag_in_loaded_templates_for_each_organization_id_BD() { OrganizationDto otherOrganization = dbTester.organizations().insert(); String[] uuids = {"uuid 1", "uuid 2"}; Long[] dates = {2_456_789L, 6_123_789L}; String[] formattedDates = {formatDateTime(new Date(dates[0])), formatDateTime(new Date(dates[1]))}; - DummyProfileDefinition qpDefinition = new DummyProfileDefinition("foo", "foo1", false); - RegisterQualityProfiles underTest = mockedEs(Collections.singletonList(qpDefinition), new Languages(FOO_LANGUAGE)); + DefinedQProfile definedQProfile = definedQProfileRepositoryRule.add(FOO_LANGUAGE, "foo1"); mockForQPInserts(uuids, dates); underTest.start(); + assertThat(definedQProfileRepositoryRule.isInitialized()).isTrue(); Arrays.asList(dbTester.getDefaultOrganization(), otherOrganization) .forEach(organization -> { QualityProfileDto dto = getPersistedQP(organization, FOO_LANGUAGE, "foo1"); @@ -140,111 +108,65 @@ public class RegisterQualityProfilesTest { assertThat(dto.getRulesUpdatedAt()).isIn(formattedDates); assertThat(dto.getLastUsed()).isNull(); assertThat(dto.getUserUpdatedAt()).isNull(); - assertThat(dto.isDefault()).isTrue(); + assertThat(dto.isDefault()).isFalse(); - assertThat(dbClient.loadedTemplateDao().countByTypeAndKey(computeLoadedTemplateType(qpDefinition), organization.getUuid(), dbTester.getSession())) + assertThat(dbClient.loadedTemplateDao().countByTypeAndKey(definedQProfile.getLoadedTemplateType(), organization.getUuid(), dbTester.getSession())) .isEqualTo(1); }); assertThat(dbTester.countRowsOfTable(dbTester.getSession(), TABLE_RULES_PROFILES)).isEqualTo(2); } @Test - public void start_makes_single_qp_of_a_language_default_even_if_not_flagged_as_so() { - RegisterQualityProfiles underTest = mockedEs(Collections.singletonList(new DummyProfileDefinition("foo", "foo1", false)), new Languages(FOO_LANGUAGE)); - mockForSingleQPInsert(); + public void start_persists_default_flag_of_DefinedQualityProfile() { + definedQProfileRepositoryRule.add(FOO_LANGUAGE, "foo1", false); + definedQProfileRepositoryRule.add(FOO_LANGUAGE, "foo2", true); + definedQProfileRepositoryRule.add(BAR_LANGUAGE, "bar1", true); + definedQProfileRepositoryRule.add(BAR_LANGUAGE, "bar2", false); + mockForQPInserts(new String[]{"uuid1", "uuid2", "uuid3", "uuid4"}, new Long[]{ 1L, 2L, 3L, 4L}); underTest.start(); - assertThat(getPersistedQP(dbTester.getDefaultOrganization(), FOO_LANGUAGE, "foo1").isDefault()).isTrue(); - } - - @Test - public void start_makes_single_qp_of_a_language_default_even_if_flagged_as_so() { - RegisterQualityProfiles underTest = mockedEs(Collections.singletonList(new DummyProfileDefinition("foo", "foo1", true)), new Languages(FOO_LANGUAGE)); - mockForSingleQPInsert(); - - underTest.start(); - - assertThat(getPersistedQP(dbTester.getDefaultOrganization(), FOO_LANGUAGE, "foo1").isDefault()).isTrue(); - } - - @Test - public void start_makes_first_qp_of_a_language_default_when_none_flagged_as_so() { - List definitions = new ArrayList<>(asList(new DummyProfileDefinition("foo", "foo1", false), new DummyProfileDefinition("foo", "foo2", false))); - Collections.shuffle(definitions); - String firstQPName = ((DummyProfileDefinition) definitions.iterator().next()).getName(); - RegisterQualityProfiles underTest = mockedEs(definitions, new Languages(FOO_LANGUAGE)); - mockForTwoQPInserts(); - - underTest.start(); - - QualityProfileDto dto = getPersistedQP(dbTester.getDefaultOrganization(), FOO_LANGUAGE, "foo1"); - assertThat(dto.isDefault()).isEqualTo(dto.getName().equals(firstQPName)); - dto = getPersistedQP(dbTester.getDefaultOrganization(), FOO_LANGUAGE, "foo2"); - assertThat(dto.isDefault()).isEqualTo(dto.getName().equals(firstQPName)); - assertThat(dbTester.countRowsOfTable(dbTester.getSession(), TABLE_RULES_PROFILES)).isEqualTo(2); + assertThat(definedQProfileRepositoryRule.isInitialized()).isTrue(); + assertThat(getPersistedQP(dbTester.getDefaultOrganization(), FOO_LANGUAGE, "foo1").isDefault()).isFalse(); + assertThat(getPersistedQP(dbTester.getDefaultOrganization(), FOO_LANGUAGE, "foo2").isDefault()).isTrue(); + assertThat(getPersistedQP(dbTester.getDefaultOrganization(), BAR_LANGUAGE, "bar1").isDefault()).isTrue(); + assertThat(getPersistedQP(dbTester.getDefaultOrganization(), BAR_LANGUAGE, "bar2").isDefault()).isFalse(); } @Test public void start_does_not_create_sq_if_loaded_profile_of_organization_already_exists() { OrganizationDto org1 = dbTester.organizations().insert(); OrganizationDto org2 = dbTester.organizations().insert(); - DummyProfileDefinition qpDefinition = new DummyProfileDefinition("foo", "foo1", false); - dbClient.loadedTemplateDao().insert(new LoadedTemplateDto(dbTester.getDefaultOrganization().getUuid(), computeLoadedTemplateType(qpDefinition)), dbTester.getSession()); - dbClient.loadedTemplateDao().insert(new LoadedTemplateDto(org1.getUuid(), computeLoadedTemplateType(qpDefinition)), dbTester.getSession()); + DefinedQProfile definedQProfile = definedQProfileRepositoryRule.add(FOO_LANGUAGE, "foo1"); + dbClient.loadedTemplateDao().insert(new LoadedTemplateDto(dbTester.getDefaultOrganization().getUuid(), definedQProfile.getLoadedTemplateType()), dbTester.getSession()); + dbClient.loadedTemplateDao().insert(new LoadedTemplateDto(org1.getUuid(), definedQProfile.getLoadedTemplateType()), dbTester.getSession()); dbTester.commit(); - RegisterQualityProfiles underTest = mockedEs(Collections.singletonList(qpDefinition), new Languages(FOO_LANGUAGE)); mockForSingleQPInsert(); underTest.start(); - assertThat(dbClient.loadedTemplateDao().countByTypeAndKey(computeLoadedTemplateType(qpDefinition), org2.getUuid(), dbTester.getSession())) + assertThat(definedQProfileRepositoryRule.isInitialized()).isTrue(); + assertThat(dbClient.loadedTemplateDao().countByTypeAndKey(definedQProfile.getLoadedTemplateType(), org2.getUuid(), dbTester.getSession())) .isEqualTo(1); assertThat(dbTester.countRowsOfTable(dbTester.getSession(), TABLE_LOADED_TEMPLATES)).isEqualTo(3); assertThat(dbTester.countRowsOfTable(dbTester.getSession(), TABLE_RULES_PROFILES)).isEqualTo(1); } @Test - public void start_fails_with_ISE_when_two_sq_with_different_name_are_default_for_the_same_language() { - RegisterQualityProfiles underTest = mockedEs( - asList(new DummyProfileDefinition("foo", "foo1", true), new DummyProfileDefinition("foo", "foo2", true)), - new Languages(FOO_LANGUAGE)); - - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("Several Quality profiles are flagged as default for the language foo: [foo1, foo2]"); - - underTest.start(); - } - - @Test - public void starts_creates_single_qp_if_several_profile_have_the_same_name_for_a_given_language() { - RegisterQualityProfiles underTest = mockedEs( - asList(new DummyProfileDefinition("foo", "foo1", true), new DummyProfileDefinition("foo", "foo1", true)), - new Languages(FOO_LANGUAGE)); - mockForSingleQPInsert(); - - underTest.start(); - - assertThat(getPersistedQP(dbTester.getDefaultOrganization(), FOO_LANGUAGE, "foo1").isDefault()).isTrue(); - assertThat(dbTester.countRowsOfTable(dbTester.getSession(), TABLE_RULES_PROFILES)).isEqualTo(1); - } - - @Test - public void start_creates_different_qp_and_their_loaded_templates_if_several_profile_has_same_name_for_different_languages() { + public void start_creates_different_qps_and_their_loaded_templates_if_several_profile_has_same_name_for_different_languages() { String uuid1 = "uuid1"; String uuid2 = "uuid2"; long date1 = 2_456_789L; long date2 = 4_231_654L; String name = "doh"; - DummyProfileDefinition qpDefinition1 = new DummyProfileDefinition("foo", name, true); - DummyProfileDefinition qpDefinition2 = new DummyProfileDefinition("bar", name, true); - RegisterQualityProfiles underTest = mockedEs(asList(qpDefinition1, qpDefinition2), new Languages(FOO_LANGUAGE, BAR_LANGUAGE)); - when(mockedUuidFactory.create()).thenReturn(uuid1).thenReturn(uuid2).thenThrow(new UnsupportedOperationException("uuidFactory should be called only twice")); - when(mockedSystem2.now()).thenReturn(date1).thenReturn(date2).thenThrow(new UnsupportedOperationException("now should be called only twice")); + DefinedQProfile definedQProfile1 = definedQProfileRepositoryRule.add(FOO_LANGUAGE, name, true); + DefinedQProfile definedQProfile2 = definedQProfileRepositoryRule.add(BAR_LANGUAGE, name, true); + mockForQPInserts(new String[]{uuid1, uuid2}, new Long[]{date1, date2}); underTest.start(); + assertThat(definedQProfileRepositoryRule.isInitialized()).isTrue(); OrganizationDto organization = dbTester.getDefaultOrganization(); QualityProfileDto dto = getPersistedQP(organization, FOO_LANGUAGE, name); String uuidQP1 = dto.getKee(); @@ -273,81 +195,16 @@ public class RegisterQualityProfilesTest { assertThat(dto.isDefault()).isTrue(); assertThat(dbTester.countRowsOfTable(dbTester.getSession(), TABLE_RULES_PROFILES)).isEqualTo(2); - assertThat(dbClient.loadedTemplateDao().countByTypeAndKey(computeLoadedTemplateType(qpDefinition1), organization.getUuid(), dbTester.getSession())) + assertThat(dbClient.loadedTemplateDao().countByTypeAndKey(definedQProfile1.getLoadedTemplateType(), organization.getUuid(), dbTester.getSession())) .isEqualTo(1); - assertThat(dbClient.loadedTemplateDao().countByTypeAndKey(computeLoadedTemplateType(qpDefinition2), organization.getUuid(), dbTester.getSession())) + assertThat(dbClient.loadedTemplateDao().countByTypeAndKey(definedQProfile2.getLoadedTemplateType(), organization.getUuid(), dbTester.getSession())) .isEqualTo(1); } - @Test - public void start_create_qp_as_default_even_if_only_one_profile_with_given_name_has_default_flag_true() { - String name = "doh"; - RegisterQualityProfiles underTest = mockedEs( - asList(new DummyProfileDefinition("foo", name, false), new DummyProfileDefinition("foo", name, true)), - new Languages(FOO_LANGUAGE)); - mockForSingleQPInsert(); - - underTest.start(); - - assertThat(getPersistedQP(dbTester.getDefaultOrganization(), FOO_LANGUAGE, name).isDefault()).isTrue(); - } - - @Test - public void start_creates_qp_Sonar_Way_as_default_if_none_other_is_defined_default_for_a_given_language() { - - RegisterQualityProfiles underTest = mockedEs( - asList( - new DummyProfileDefinition("foo", "doh", false), new DummyProfileDefinition("foo", "boo", false), - new DummyProfileDefinition("foo", SONAR_WAY_QP_NAME, false), new DummyProfileDefinition("foo", "goo", false)), - new Languages(FOO_LANGUAGE)); - mockForQPInserts(new String[] {"uuid1", "uuid2", "uuid3", "uuid4"}, new Long[] {2_456_789L, 2_456_789L, 2_456_789L, 2_456_789L}); - - underTest.start(); - - Arrays.asList("doh", "boo", "goo") - .forEach(name -> assertThat(getPersistedQP(dbTester.getDefaultOrganization(), FOO_LANGUAGE, name).isDefault()) - .describedAs("QP with name " + name + " should not be default") - .isFalse()); - assertThat(getPersistedQP(dbTester.getDefaultOrganization(), FOO_LANGUAGE, SONAR_WAY_QP_NAME).isDefault()).isTrue(); - } - - @Test - public void start_does_not_create_Sonar_Way_as_default_if_other_profile_is_defined_as_default() { - RegisterQualityProfiles underTest = mockedEs( - asList( - new DummyProfileDefinition("foo", SONAR_WAY_QP_NAME, false), new DummyProfileDefinition("foo", "goo", true)), - new Languages(FOO_LANGUAGE)); - mockForTwoQPInserts(); - - underTest.start(); - - assertThat(getPersistedQP(dbTester.getDefaultOrganization(), FOO_LANGUAGE, SONAR_WAY_QP_NAME).isDefault()).isFalse(); - assertThat(getPersistedQP(dbTester.getDefaultOrganization(), FOO_LANGUAGE, "goo").isDefault()).isTrue(); - } - - @Test - public void start_matches_Sonar_Way_default_with_case_sensitivity() { - String sonarWayInOtherCase = SONAR_WAY_QP_NAME.toUpperCase(); - RegisterQualityProfiles underTest = mockedEs( - asList( - new DummyProfileDefinition("foo", "goo", false), new DummyProfileDefinition("foo", sonarWayInOtherCase, false)), - new Languages(FOO_LANGUAGE)); - mockForTwoQPInserts(); - - underTest.start(); - - assertThat(getPersistedQP(dbTester.getDefaultOrganization(), FOO_LANGUAGE, sonarWayInOtherCase).isDefault()).isFalse(); - assertThat(getPersistedQP(dbTester.getDefaultOrganization(), FOO_LANGUAGE, "goo").isDefault()).isTrue(); - } - private void mockForSingleQPInsert() { mockForSingleQPInsert("generated uuid", 2_456_789); } - private void mockForTwoQPInserts() { - mockForQPInserts(new String[] {"uuid1", "uuid2"}, new Long[] {2_456_789L, 3_789_159L}); - } - private void mockForSingleQPInsert(String uuid, long now) { when(mockedUuidFactory.create()).thenReturn(uuid).thenThrow(new UnsupportedOperationException("uuidFactory should be called only once")); when(mockedSystem2.now()).thenReturn(now).thenThrow(new UnsupportedOperationException("now should be called only once")); @@ -367,53 +224,4 @@ public class RegisterQualityProfilesTest { return dbClient.qualityProfileDao().selectByNameAndLanguage(organization, name, language.getKey(), dbTester.getSession()); } - private RegisterQualityProfiles mockedDBAndEs(List definitions, Languages languages) { - return new RegisterQualityProfiles(mockedDbClient, null, null, definitions, languages, mockedActiveRuleIndexer); - } - - private RegisterQualityProfiles mockedEs(List definitions, Languages languages) { - return new RegisterQualityProfiles( - dbClient, - new QProfileFactory(dbClient, mockedUuidFactory, mockedSystem2), - new CachingRuleActivator(mockedSystem2, dbClient, null, new CachingRuleActivatorContextFactory(dbClient), null, null, userSessionRule), - definitions, - languages, - mockedActiveRuleIndexer); - } - - private static final class DummyProfileDefinition extends ProfileDefinition { - private final String language; - private final String name; - private final boolean defaultProfile; - - private DummyProfileDefinition(String language, String name, boolean defaultProfile) { - this.language = language; - this.name = name; - this.defaultProfile = defaultProfile; - } - - @Override - public RulesProfile createProfile(ValidationMessages validation) { - RulesProfile res = RulesProfile.create(name, language); - res.setDefaultProfile(defaultProfile); - return res; - } - - String getLanguage() { - return language; - } - - String getName() { - return name; - } - - boolean isDefaultProfile() { - return defaultProfile; - } - } - - private String computeLoadedTemplateType(DummyProfileDefinition qpDefinition) { - String qpIdentifier = lowerCase(qpDefinition.getLanguage()) + ":" + qpDefinition.getName(); - return format("%s.%s", TYPE_QUALITY_PROFILE, DigestUtils.md5Hex(qpIdentifier.getBytes(UTF_8))); - } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/rule/RegisterRulesMediumTest.java b/server/sonar-server/src/test/java/org/sonar/server/rule/RegisterRulesMediumTest.java index 527c989fc4e..acfb40a545b 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/rule/RegisterRulesMediumTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/rule/RegisterRulesMediumTest.java @@ -101,7 +101,7 @@ public class RegisterRulesMediumTest { dbSession.close(); } RULE_DEFS.set(rules); - TESTER.get(Platform.class).executeStartupTasks(); + TESTER.get(Platform.class).restart(); db = TESTER.get(DbClient.class); dbSession = TESTER.get(DbClient.class).openSession(false); dbSession.clearCache(); -- 2.39.5