/* * SonarQube * Copyright (C) 2009-2023 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.application.command; import com.google.common.collect.ImmutableMap; import com.tngtech.java.junit.dataprovider.DataProvider; import com.tngtech.java.junit.dataprovider.DataProviderRunner; import com.tngtech.java.junit.dataprovider.UseDataProvider; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Properties; import java.util.Random; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; import org.assertj.core.api.ThrowableAssert.ThrowingCallable; import org.junit.Test; import org.junit.runner.RunWith; import org.sonar.process.MessageException; import org.sonar.process.Props; import static java.lang.String.valueOf; 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.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.fail; @RunWith(DataProviderRunner.class) public class JvmOptionsTest { private final Random random = new Random(); private final String randomPropertyName = randomAlphanumeric(3); private final String randomPrefix = "-" + randomAlphabetic(5).toLowerCase(Locale.ENGLISH); private final String randomValue = randomAlphanumeric(4).toLowerCase(Locale.ENGLISH); private final Properties properties = new Properties(); private final JvmOptions underTest = new JvmOptions(); @Test public void constructor_without_arguments_creates_empty_JvmOptions() { JvmOptions testJvmOptions = new JvmOptions<>(); assertThat(testJvmOptions.getAll()).isEmpty(); } @Test public void constructor_throws_NPE_if_argument_is_null() { expectJvmOptionNotNullNPE(() -> new JvmOptions(null)); } @Test public void constructor_throws_NPE_if_any_option_prefix_is_null() { Map mandatoryJvmOptions = shuffleThenToMap( Stream.of( IntStream.range(0, random.nextInt(10)).mapToObj(i -> new Option("-B", valueOf(i))), Stream.of(new Option(null, "value"))) .flatMap(s -> s)); assertThatThrownBy(() -> new JvmOptions(mandatoryJvmOptions)) .isInstanceOf(NullPointerException.class) .hasMessage("JVM option prefix can't be null"); } @Test @UseDataProvider("variousEmptyStrings") public void constructor_throws_IAE_if_any_option_prefix_is_empty(String emptyString) { Map mandatoryJvmOptions = shuffleThenToMap( Stream.of( IntStream.range(0, random.nextInt(10)).mapToObj(i -> new Option("-B", valueOf(i))), Stream.of(new Option(emptyString, "value"))) .flatMap(s -> s)); assertThatThrownBy(() -> new JvmOptions(mandatoryJvmOptions)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("JVM option prefix can't be empty"); } @Test public void constructor_throws_IAE_if_any_option_prefix_does_not_start_with_dash() { String invalidPrefix = randomAlphanumeric(3); Map mandatoryJvmOptions = shuffleThenToMap( Stream.of( IntStream.range(0, random.nextInt(10)).mapToObj(i -> new Option("-B", valueOf(i))), Stream.of(new Option(invalidPrefix, "value"))) .flatMap(s -> s)); expectJvmOptionNotEmptyAndStartByDashIAE(() -> new JvmOptions(mandatoryJvmOptions)); } @Test public void constructor_throws_NPE_if_any_option_value_is_null() { Map mandatoryJvmOptions = shuffleThenToMap( Stream.of( IntStream.range(0, random.nextInt(10)).mapToObj(i -> new Option("-B", valueOf(i))), Stream.of(new Option("-prefix", null))) .flatMap(s -> s)); assertThatThrownBy(() -> new JvmOptions(mandatoryJvmOptions)) .isInstanceOf(NullPointerException.class) .hasMessage("JVM option value can't be null"); } @Test @UseDataProvider("variousEmptyStrings") public void constructor_accepts_any_empty_option_value(String emptyString) { Map mandatoryJvmOptions = shuffleThenToMap( Stream.of( IntStream.range(0, random.nextInt(10)).mapToObj(i -> new Option("-B", valueOf(i))), Stream.of(new Option("-prefix", emptyString))) .flatMap(s -> s)); new JvmOptions(mandatoryJvmOptions); } @Test public void add_throws_NPE_if_argument_is_null() { expectJvmOptionNotNullNPE(() -> underTest.add(null)); } @Test @UseDataProvider("variousEmptyStrings") public void add_throws_IAE_if_argument_is_empty(String emptyString) { expectJvmOptionNotEmptyAndStartByDashIAE(() -> underTest.add(emptyString)); } @Test public void add_throws_IAE_if_argument_does_not_start_with_dash() { expectJvmOptionNotEmptyAndStartByDashIAE(() -> underTest.add(randomAlphanumeric(3))); } @Test @UseDataProvider("variousEmptyStrings") public void add_adds_with_trimming(String emptyString) { underTest.add(emptyString + "-foo" + emptyString); assertThat(underTest.getAll()).containsOnly("-foo"); } @Test public void add_throws_MessageException_if_option_starts_with_prefix_of_mandatory_option_but_has_different_value() { String[] optionOverrides = { randomPrefix, randomPrefix + randomAlphanumeric(1), randomPrefix + randomAlphanumeric(2), randomPrefix + randomAlphanumeric(3), randomPrefix + randomAlphanumeric(4), randomPrefix + randomValue.substring(1), randomPrefix + randomValue.substring(2), randomPrefix + randomValue.substring(3) }; JvmOptions underTest = new JvmOptions(ImmutableMap.of(randomPrefix, randomValue)); for (String optionOverride : optionOverrides) { try { underTest.add(optionOverride); fail("an MessageException should have been thrown"); } catch (MessageException e) { assertThat(e.getMessage()).isEqualTo("a JVM option can't overwrite mandatory JVM options. " + optionOverride + " overwrites " + randomPrefix + randomValue); } } } @Test public void add_checks_against_mandatory_options_is_case_sensitive() { String[] optionOverrides = { randomPrefix, randomPrefix + randomAlphanumeric(1), randomPrefix + randomAlphanumeric(2), randomPrefix + randomAlphanumeric(3), randomPrefix + randomAlphanumeric(4), randomPrefix + randomValue.substring(1), randomPrefix + randomValue.substring(2), randomPrefix + randomValue.substring(3) }; JvmOptions underTest = new JvmOptions(ImmutableMap.of(randomPrefix, randomValue)); for (String optionOverride : optionOverrides) { underTest.add(optionOverride.toUpperCase(Locale.ENGLISH)); } } @Test public void add_accepts_property_equal_to_mandatory_option_and_does_not_add_it_twice() { JvmOptions underTest = new JvmOptions(ImmutableMap.of(randomPrefix, randomValue)); underTest.add(randomPrefix + randomValue); assertThat(underTest.getAll()).containsOnly(randomPrefix + randomValue); } @Test public void addFromMandatoryProperty_fails_with_IAE_if_property_does_not_exist() { expectMissingPropertyIAE(() -> underTest.addFromMandatoryProperty(new Props(properties), this.randomPropertyName), this.randomPropertyName); } @Test public void addFromMandatoryProperty_fails_with_IAE_if_property_contains_an_empty_value() { expectMissingPropertyIAE(() -> underTest.addFromMandatoryProperty(new Props(properties), randomPropertyName), this.randomPropertyName); } @Test @UseDataProvider("variousEmptyStrings") public void addFromMandatoryProperty_adds_single_option_of_property_with_trimming(String emptyString) { properties.put(randomPropertyName, emptyString + "-foo" + emptyString); underTest.addFromMandatoryProperty(new Props(properties), randomPropertyName); assertThat(underTest.getAll()).containsOnly("-foo"); } @Test @UseDataProvider("variousEmptyStrings") public void addFromMandatoryProperty_fails_with_MessageException_if_property_does_not_start_with_dash_after_trimmed(String emptyString) { properties.put(randomPropertyName, emptyString + "foo -bar"); expectJvmOptionNotEmptyAndStartByDashMessageException(() -> underTest.addFromMandatoryProperty(new Props(properties), randomPropertyName), randomPropertyName, "foo"); } @Test @UseDataProvider("variousEmptyStrings") public void addFromMandatoryProperty_adds_options_of_property_with_trimming(String emptyString) { properties.put(randomPropertyName, emptyString + "-foo" + emptyString + " -bar" + emptyString + " -duck" + emptyString); underTest.addFromMandatoryProperty(new Props(properties), randomPropertyName); assertThat(underTest.getAll()).containsOnly("-foo", "-bar", "-duck"); } @Test public void addFromMandatoryProperty_supports_spaces_inside_options() { properties.put(randomPropertyName, "-foo bar -duck"); underTest.addFromMandatoryProperty(new Props(properties), randomPropertyName); assertThat(underTest.getAll()).containsOnly("-foo bar", "-duck"); } @Test public void addFromMandatoryProperty_throws_IAE_if_option_starts_with_prefix_of_mandatory_option_but_has_different_value() { String[] optionOverrides = { randomPrefix, randomPrefix + randomValue.substring(1), randomPrefix + randomValue.substring(1), randomPrefix + randomValue.substring(2), randomPrefix + randomValue.substring(3), randomPrefix + randomValue.substring(3) + randomAlphanumeric(1), randomPrefix + randomValue.substring(3) + randomAlphanumeric(2), randomPrefix + randomValue.substring(3) + randomAlphanumeric(3), randomPrefix + randomValue + randomAlphanumeric(1) }; JvmOptions underTest = new JvmOptions(ImmutableMap.of(randomPrefix, randomValue)); for (String optionOverride : optionOverrides) { try { properties.put(randomPropertyName, optionOverride); underTest.addFromMandatoryProperty(new Props(properties), randomPropertyName); fail("an MessageException should have been thrown"); } catch (MessageException e) { assertThat(e.getMessage()) .isEqualTo("a JVM option can't overwrite mandatory JVM options. " + "The following JVM options defined by property '" + randomPropertyName + "' are invalid: " + optionOverride + " overwrites " + randomPrefix + randomValue); } } } @Test public void addFromMandatoryProperty_checks_against_mandatory_options_is_case_sensitive() { String[] optionOverrides = { randomPrefix, randomPrefix + randomValue.substring(1), randomPrefix + randomValue.substring(1), randomPrefix + randomValue.substring(2), randomPrefix + randomValue.substring(3), randomPrefix + randomValue.substring(3) + randomAlphanumeric(1), randomPrefix + randomValue.substring(3) + randomAlphanumeric(2), randomPrefix + randomValue.substring(3) + randomAlphanumeric(3), randomPrefix + randomValue + randomAlphanumeric(1) }; JvmOptions underTest = new JvmOptions(ImmutableMap.of(randomPrefix, randomValue)); for (String optionOverride : optionOverrides) { properties.setProperty(randomPropertyName, optionOverride.toUpperCase(Locale.ENGLISH)); underTest.addFromMandatoryProperty(new Props(properties), randomPropertyName); } } @Test public void addFromMandatoryProperty_reports_all_overriding_options_in_single_exception() { String overriding1 = randomPrefix; String overriding2 = randomPrefix + randomValue + randomAlphanumeric(1); properties.setProperty(randomPropertyName, "-foo " + overriding1 + " -bar " + overriding2); JvmOptions underTest = new JvmOptions(ImmutableMap.of(randomPrefix, randomValue)); assertThatThrownBy(() -> underTest.addFromMandatoryProperty(new Props(properties), randomPropertyName)) .isInstanceOf(MessageException.class) .hasMessage("a JVM option can't overwrite mandatory JVM options. " + "The following JVM options defined by property '" + randomPropertyName + "' are invalid: " + overriding1 + " overwrites " + randomPrefix + randomValue + ", " + overriding2 + " overwrites " + randomPrefix + randomValue); } @Test public void addFromMandatoryProperty_accepts_property_equal_to_mandatory_option_and_does_not_add_it_twice() { JvmOptions underTest = new JvmOptions(ImmutableMap.of(randomPrefix, randomValue)); properties.put(randomPropertyName, randomPrefix + randomValue); underTest.addFromMandatoryProperty(new Props(properties), randomPropertyName); assertThat(underTest.getAll()).containsOnly(randomPrefix + randomValue); } @Test public void toString_prints_all_jvm_options() { underTest.add("-foo").add("-bar"); assertThat(underTest).hasToString("[-foo, -bar]"); } private void expectJvmOptionNotNullNPE(ThrowingCallable callback) { assertThatThrownBy(callback) .isInstanceOf(NullPointerException.class) .hasMessage("a JVM option can't be null"); } private void expectJvmOptionNotEmptyAndStartByDashIAE(ThrowingCallable callback) { assertThatThrownBy(callback) .isInstanceOf(IllegalArgumentException.class) .hasMessage("a JVM option can't be empty and must start with '-'"); } private void expectJvmOptionNotEmptyAndStartByDashMessageException(ThrowingCallable callback, String randomPropertyName, String option) { assertThatThrownBy(callback) .isInstanceOf(MessageException.class) .hasMessage("a JVM option can't be empty and must start with '-'. " + "The following JVM options defined by property '" + randomPropertyName + "' are invalid: " + option); } public void expectMissingPropertyIAE(ThrowingCallable callback, String randomPropertyName) { assertThatThrownBy(callback) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Missing property: " + randomPropertyName); } @DataProvider() public static Object[][] variousEmptyStrings() { return new Object[][] { {""}, {" "}, {" "} }; } private static Map shuffleThenToMap(Stream