diff options
| author | Elena Vilchik | 2019-12-18 17:10:10 +0100 |
|---|---|---|
| committer | Alban Auzeill | 2019-12-18 17:10:10 +0100 |
| commit | c8f0071c4f5336dfe0efc5d3c218ab49f2401264 (patch) | |
| tree | 254cd5ed9531d7c62bab4f8ec082e085795ecb8f /sonar-css-plugin/src/test/java/org/sonar/css | |
| parent | 13fe08e87c8a70ffe6e248b774ef826bbe1f779d (diff) | |
| download | sonar-css-c8f0071c4f5336dfe0efc5d3c218ab49f2401264.tar.bz2 | |
Rely on NodeJS API of Stylelint to execute CSS rules (#221)
Diffstat (limited to 'sonar-css-plugin/src/test/java/org/sonar/css')
9 files changed, 531 insertions, 351 deletions
diff --git a/sonar-css-plugin/src/test/java/org/sonar/css/plugin/AnalysisWarningsWrapperTest.java b/sonar-css-plugin/src/test/java/org/sonar/css/plugin/AnalysisWarningsWrapperTest.java deleted file mode 100644 index fc93670..0000000 --- a/sonar-css-plugin/src/test/java/org/sonar/css/plugin/AnalysisWarningsWrapperTest.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * SonarCSS - * Copyright (C) 2018-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.css.plugin; - -import org.junit.Test; -import org.sonar.api.notifications.AnalysisWarnings; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - -public class AnalysisWarningsWrapperTest { - @Test - public void delegate_to_analysisWarnings() { - AnalysisWarnings analysisWarnings = mock(AnalysisWarnings.class); - - AnalysisWarningsWrapper wrapper = new AnalysisWarningsWrapper(analysisWarnings); - - String warning = "some warning"; - wrapper.addUnique(warning); - verify(analysisWarnings).addUnique(warning); - } -} diff --git a/sonar-css-plugin/src/test/java/org/sonar/css/plugin/CssPluginTest.java b/sonar-css-plugin/src/test/java/org/sonar/css/plugin/CssPluginTest.java index a2e420e..b0f1b28 100644 --- a/sonar-css-plugin/src/test/java/org/sonar/css/plugin/CssPluginTest.java +++ b/sonar-css-plugin/src/test/java/org/sonar/css/plugin/CssPluginTest.java @@ -33,16 +33,7 @@ public class CssPluginTest { @Test public void count_extensions() { - SonarRuntime runtime = SonarRuntimeImpl.forSonarQube(Version.create(6, 7), SonarQubeSide.SCANNER, SonarEdition.COMMUNITY); - Plugin.Context context = new Plugin.Context(runtime); - Plugin underTest = new CssPlugin(); - underTest.define(context); - assertThat(context.getExtensions()).hasSize(10); - } - - @Test - public void count_extensions_7_2() { - SonarRuntime runtime = SonarRuntimeImpl.forSonarQube(Version.create(7, 2), SonarQubeSide.SCANNER, SonarEdition.COMMUNITY); + SonarRuntime runtime = SonarRuntimeImpl.forSonarQube(Version.create(7, 9), SonarQubeSide.SCANNER, SonarEdition.COMMUNITY); Plugin.Context context = new Plugin.Context(runtime); Plugin underTest = new CssPlugin(); underTest.define(context); @@ -50,17 +41,8 @@ public class CssPluginTest { } @Test - public void count_extensions_7_4() { - SonarRuntime runtime = SonarRuntimeImpl.forSonarQube(Version.create(7, 4), SonarQubeSide.SCANNER, SonarEdition.COMMUNITY); - Plugin.Context context = new Plugin.Context(runtime); - Plugin underTest = new CssPlugin(); - underTest.define(context); - assertThat(context.getExtensions()).hasSize(12); - } - - @Test - public void count_extensions_7_4_sonarlint() { - SonarRuntime runtime = SonarRuntimeImpl.forSonarLint(Version.create(7, 4)); + public void count_extensions_sonarlint() { + SonarRuntime runtime = SonarRuntimeImpl.forSonarLint(Version.create(7, 9)); Plugin.Context context = new Plugin.Context(runtime); Plugin underTest = new CssPlugin(); underTest.define(context); diff --git a/sonar-css-plugin/src/test/java/org/sonar/css/plugin/CssRuleSensorTest.java b/sonar-css-plugin/src/test/java/org/sonar/css/plugin/CssRuleSensorTest.java index a0a41f8..0dd35d8 100644 --- a/sonar-css-plugin/src/test/java/org/sonar/css/plugin/CssRuleSensorTest.java +++ b/sonar-css-plugin/src/test/java/org/sonar/css/plugin/CssRuleSensorTest.java @@ -21,41 +21,40 @@ package org.sonar.css.plugin; import java.io.File; import java.io.IOException; +import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.concurrent.TimeUnit; -import java.util.function.Consumer; -import javax.annotation.Nullable; -import org.awaitility.Awaitility; +import java.util.Collections; +import org.junit.After; 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.api.batch.fs.InputFile; import org.sonar.api.batch.fs.InputFile.Type; import org.sonar.api.batch.fs.internal.DefaultInputFile; import org.sonar.api.batch.fs.internal.TestInputFileBuilder; import org.sonar.api.batch.rule.CheckFactory; -import org.sonar.api.batch.sensor.SensorContext; import org.sonar.api.batch.sensor.internal.DefaultSensorDescriptor; import org.sonar.api.batch.sensor.internal.SensorContextTester; -import org.sonar.api.batch.sensor.issue.Issue; +import org.sonar.api.notifications.AnalysisWarnings; import org.sonar.api.utils.log.LogTester; import org.sonar.api.utils.log.LoggerLevel; -import org.sonar.css.plugin.bundle.BundleHandler; -import org.sonar.css.plugin.bundle.CssBundleHandler; -import org.sonarsource.nodejs.NodeCommand; -import org.sonarsource.nodejs.NodeCommandException; +import org.sonar.css.plugin.server.CssAnalyzerBridgeServer; import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.await; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.eq; 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; +import static org.sonar.css.plugin.server.CssAnalyzerBridgeServerTest.createCssAnalyzerBridgeServer; public class CssRuleSensorTest { @@ -66,235 +65,212 @@ public class CssRuleSensorTest { public TemporaryFolder tmpDir = new TemporaryFolder(); @Rule - public ExpectedException thrown= ExpectedException.none(); + public ExpectedException thrown = ExpectedException.none(); - private static CheckFactory checkFactory = new CheckFactory(new TestActiveRules("S4647", "S4656")); + private static final CheckFactory CHECK_FACTORY = new CheckFactory(new TestActiveRules("S4647", "S4656", "S4658")); private static final File BASE_DIR = new File("src/test/resources").getAbsoluteFile(); private SensorContextTester context = SensorContextTester.create(BASE_DIR); - private DefaultInputFile inputFile = createInputFile(context, "some css content\n on 2 lines", "dir/file.css"); - private AnalysisWarningsWrapper analysisWarnings = mock(AnalysisWarningsWrapper.class); + private AnalysisWarnings analysisWarnings = mock(AnalysisWarnings.class); + private CssAnalyzerBridgeServer cssAnalyzerBridgeServer; + private CssRuleSensor sensor; @Before public void setUp() { context.fileSystem().setWorkDir(tmpDir.getRoot().toPath()); - Awaitility.setDefaultTimeout(5, TimeUnit.MINUTES); + cssAnalyzerBridgeServer = createCssAnalyzerBridgeServer(); + sensor = new CssRuleSensor(CHECK_FACTORY, cssAnalyzerBridgeServer, analysisWarnings); + } + + @After + public void tearDown() throws Exception { + if (cssAnalyzerBridgeServer != null) { + cssAnalyzerBridgeServer.stop(); + } } @Test public void test_descriptor() { - CssRuleSensor sensor = new CssRuleSensor(new CssBundleHandler(), checkFactory, new StylelintCommandProvider(), analysisWarnings); DefaultSensorDescriptor sensorDescriptor = new DefaultSensorDescriptor(); sensor.describe(sensorDescriptor); assertThat(sensorDescriptor.name()).isEqualTo("SonarCSS Rules"); assertThat(sensorDescriptor.languages()).isEmpty(); + assertThat(sensorDescriptor.configurationPredicate()).isNull(); + assertThat(sensorDescriptor.ruleRepositories()).containsOnly("css"); } @Test public void test_execute() throws IOException { - TestLinterCommandProvider commandProvider = getCommandProvider(); - CssRuleSensor sensor = createCssRuleSensor(commandProvider); + addInputFile("file.css"); + addInputFile("file-with-rule-id-message.css"); sensor.execute(context); - assertThat(context.allIssues()).hasSize(1); - Issue issue = context.allIssues().iterator().next(); - assertThat(issue.primaryLocation().message()).isEqualTo("some message"); - - Path configPath = Paths.get(context.fileSystem().workDir().getAbsolutePath(), "testconfig.json"); - assertThat(Files.readAllLines(configPath)).containsOnly("{\"rules\":{\"color-no-invalid-hex\":true,\"declaration-block-no-duplicate-properties\":[true,{\"ignore\":[\"consecutive-duplicates-with-different-values\"]}]}}"); + assertThat(context.allIssues()).hasSize(2); + assertThat(context.allIssues()).extracting("primaryLocation.message") + .containsOnly("some message", "Unexpected empty block"); + + assertThat(String.join("\n", logTester.logs(LoggerLevel.DEBUG))) + .matches("(?s).*Analyzing \\S*file\\.css.*") + .matches("(?s).*Found 1 issue\\(s\\).*"); + + Path configPath = Paths.get(context.fileSystem().workDir().getAbsolutePath(), "css-bundle", "stylelintconfig.json"); + assertThat(Files.readAllLines(configPath)).containsOnly( + "{\"rules\":{" + + "\"block-no-empty\":true," + + "\"color-no-invalid-hex\":true," + + "\"declaration-block-no-duplicate-properties\":[true,{\"ignore\":[\"consecutive-duplicates-with-different-values\"]}]" + + "}}"); verifyZeroInteractions(analysisWarnings); } @Test - public void test_old_property_is_provided() { - TestLinterCommandProvider commandProvider = getCommandProvider(); - CssRuleSensor sensor = createCssRuleSensor(commandProvider, analysisWarnings); - context.settings().setProperty(CssPlugin.FORMER_NODE_EXECUTABLE, "foo"); - sensor.execute(context); + public void test_non_css_files() { + DefaultInputFile fileCss = addInputFile("file.css"); + DefaultInputFile fileHtml = addInputFile("file.web"); + DefaultInputFile filePhp = addInputFile("file.php"); + addInputFile("file.js"); - assertThat(logTester.logs(LoggerLevel.WARN)).contains("Property 'sonar.css.node' is ignored, 'sonar.nodejs.executable' should be used instead"); - verify(analysisWarnings).addUnique(eq("Property 'sonar.css.node' is ignored, 'sonar.nodejs.executable' should be used instead")); + sensor.execute(context); - assertThat(context.allIssues()).hasSize(1); + assertThat(context.allIssues()).hasSize(3); + assertThat(context.allIssues()) + .extracting("primaryLocation.component") + .containsOnly(fileCss, fileHtml, filePhp); } @Test - public void test_invalid_node() { - InvalidCommandProvider commandProvider = new InvalidCommandProvider(); - CssRuleSensor sensor = createCssRuleSensor(commandProvider); + public void test_no_file_to_analyze() throws IOException { sensor.execute(context); - assertThat(context.allIssues()).hasSize(0); - assertThat(logTester.logs(LoggerLevel.ERROR)).contains("Some problem happened. No CSS files will be analyzed."); - verifyZeroInteractions(analysisWarnings); + assertThat(logTester.logs(LoggerLevel.ERROR)).isEmpty(); + assertThat(logTester.logs(LoggerLevel.INFO)).contains("No CSS, PHP or HTML files are found in the project. CSS analysis is skipped."); } @Test - public void test_execute_with_analysisWarnings() throws IOException { - TestLinterCommandProvider commandProvider = getCommandProvider(); - CssRuleSensor sensor = createCssRuleSensor(commandProvider, analysisWarnings); + public void bridge_server_fail_to_start() { + CssAnalyzerBridgeServer badServer = createCssAnalyzerBridgeServer("throw.js"); + sensor = new CssRuleSensor(CHECK_FACTORY, badServer, analysisWarnings); + addInputFile("file.css"); sensor.execute(context); + assertThat(logTester.logs(LoggerLevel.ERROR)).contains("Failed to start server (1s timeout)"); - assertThat(context.allIssues()).hasSize(1); - Issue issue = context.allIssues().iterator().next(); - assertThat(issue.primaryLocation().message()).isEqualTo("some message"); - - Path configPath = Paths.get(context.fileSystem().workDir().getAbsolutePath(), "testconfig.json"); - assertThat(Files.readAllLines(configPath)).containsOnly("{\"rules\":{\"color-no-invalid-hex\":true,\"declaration-block-no-duplicate-properties\":[true,{\"ignore\":[\"consecutive-duplicates-with-different-values\"]}]}}"); - verifyZeroInteractions(analysisWarnings); + assertThat(logTester.logs(LoggerLevel.DEBUG)) + .doesNotContain("Skipping start of css-bundle server due to the failure during first analysis"); + sensor.execute(context); + assertThat(logTester.logs(LoggerLevel.DEBUG)) + .contains("Skipping start of css-bundle server due to the failure during first analysis"); } @Test - public void test_invalid_node_command_with_analysisWarnings() { - InvalidCommandProvider commandProvider = new InvalidCommandProvider(); - CssRuleSensor sensor = createCssRuleSensor(commandProvider, analysisWarnings); + public void should_log_when_bridge_server_receives_invalid_response() { + addInputFile("invalid-json-response.css"); + addInputFile("file.css"); sensor.execute(context); + assertThat(String.join("\n", logTester.logs(LoggerLevel.ERROR))) + .contains("Failed to parse response"); + assertThat(context.allIssues()).hasSize(1); + } - assertThat(context.allIssues()).hasSize(0); - assertThat(logTester.logs(LoggerLevel.ERROR)).contains("Some problem happened. No CSS files will be analyzed."); - verify(analysisWarnings).addUnique(eq("CSS files were not analyzed. Some problem happened.")); + @Test + public void should_fail_fast_when_server_fail_to_start() { + context.settings().setProperty("sonar.internal.analysis.failFast", "true"); + CssAnalyzerBridgeServer badServer = createCssAnalyzerBridgeServer("throw.js"); + sensor = new CssRuleSensor(CHECK_FACTORY, badServer, analysisWarnings); + addInputFile("file.css"); + + assertThatThrownBy(() -> sensor.execute(context)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Analysis failed"); } @Test - public void test_error() { - TestLinterCommandProvider commandProvider = new TestLinterCommandProvider().nodeScript("/executables/mockError.js", inputFile.absolutePath()); - CssRuleSensor sensor = createCssRuleSensor(commandProvider); - sensor.execute(context); + public void should_fail_fast_when_bridge_server_receives_invalid_response() { + context.settings().setProperty("sonar.internal.analysis.failFast", "true"); + addInputFile("invalid-json-response.css"); + + assertThatThrownBy(() -> sensor.execute(context)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Analysis failed"); + } - assertThat(logTester.logs(LoggerLevel.ERROR)).anyMatch(s -> s.startsWith("Failed to run external linting process")); + @Test + public void should_not_analyze_files_with_not_file_uri() throws URISyntaxException, IOException { + InputFile httpFile = mock(InputFile.class); + when(httpFile.filename()).thenReturn("file.css"); + when(httpFile.uri()).thenReturn(new URI("http://lost-on-earth.com/file.css")); + sensor.analyzeFile(context, httpFile, new File("config.json")); + assertThat(String.join("\n", logTester.logs(LoggerLevel.DEBUG))) + .matches("(?s).*Skipping \\S*file.css as it has not 'file' scheme.*") + .doesNotMatch("(?s).*\nAnalyzing \\S*file.css.*"); } @Test - public void test_not_execute_rules_if_nothing_enabled() { - TestLinterCommandProvider commandProvider = new TestLinterCommandProvider().nodeScript("/executables/mockError.js", inputFile.absolutePath()); - CssRuleSensor sensor = new CssRuleSensor(new TestBundleHandler(), new CheckFactory(new TestActiveRules()), commandProvider, analysisWarnings); + public void analysis_stop_when_server_is_not_anymore_alive() throws IOException, InterruptedException { + File configFile = new File("config.json"); + DefaultInputFile inputFile = addInputFile("dir/file.css"); sensor.execute(context); + cssAnalyzerBridgeServer.setPort(43); - assertThat(logTester.logs(LoggerLevel.WARN)).contains("No rules are activated in CSS Quality Profile"); + assertThatThrownBy(() -> sensor.analyzeFiles(context, Collections.singletonList(inputFile), configFile)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("css-bundle server is not answering"); } @Test - public void test_stylelint_throws() { - TestLinterCommandProvider commandProvider = new TestLinterCommandProvider().nodeScript("/executables/mockThrow.js", inputFile.absolutePath()); - CssRuleSensor sensor = createCssRuleSensor(commandProvider); + public void should_stop_execution_when_sensor_context_is_cancelled() throws IOException { + addInputFile("file.css"); + context.setCancelled(true); sensor.execute(context); - - await().until(() -> logTester.logs(LoggerLevel.ERROR) - .contains("throw new Error('houps!');")); + assertThat(context.allIssues()).isEmpty(); + assertThat(logTester.logs(LoggerLevel.INFO)) + .contains("java.util.concurrent.CancellationException: Analysis interrupted because the SensorContext is in cancelled state"); } @Test - public void test_stylelint_exitvalue() { - TestLinterCommandProvider commandProvider = new TestLinterCommandProvider().nodeScript("/executables/mockExit.js", "1"); - CssRuleSensor sensor = createCssRuleSensor(commandProvider); + public void test_old_property_is_provided() { + context.settings().setProperty(CssPlugin.FORMER_NODE_EXECUTABLE, "foo"); + addInputFile("file.css"); sensor.execute(context); - await().until(() -> logTester.logs(LoggerLevel.ERROR) - .contains("Analysis didn't terminate normally, please verify ERROR and WARN logs above. Exit code 1")); + assertThat(logTester.logs(LoggerLevel.WARN)).contains("Property 'sonar.css.node' is ignored, 'sonar.nodejs.executable' should be used instead"); + verify(analysisWarnings).addUnique(eq("Property 'sonar.css.node' is ignored, 'sonar.nodejs.executable' should be used instead")); + + assertThat(context.allIssues()).hasSize(1); + + sensor = new CssRuleSensor(CHECK_FACTORY, cssAnalyzerBridgeServer, null); + sensor.execute(context); + verifyNoMoreInteractions(analysisWarnings); } @Test public void test_syntax_error() { - SensorContextTester context = SensorContextTester.create(BASE_DIR); - context.fileSystem().setWorkDir(tmpDir.getRoot().toPath()); - DefaultInputFile inputFile = createInputFile(context, "some css content\n on 2 lines", "dir/file.css"); - TestLinterCommandProvider rulesExecution = new TestLinterCommandProvider().nodeScript("/executables/mockSyntaxError.js", inputFile.absolutePath()); - CssRuleSensor sensor = createCssRuleSensor(rulesExecution); + InputFile inputFile = addInputFile("syntax-error.css"); sensor.execute(context); - + assertThat(context.allIssues()).isEmpty(); assertThat(logTester.logs(LoggerLevel.ERROR)).contains("Failed to parse " + inputFile.uri() + ", line 2, Missed semicolon"); } @Test public void test_unknown_rule() { - SensorContextTester context = SensorContextTester.create(BASE_DIR); - context.fileSystem().setWorkDir(tmpDir.getRoot().toPath()); - DefaultInputFile inputFile = createInputFile(context, "some css content\n on 2 lines", "dir/file.css"); - TestLinterCommandProvider rulesExecution = new TestLinterCommandProvider().nodeScript("/executables/mockUnknownRule.js", inputFile.absolutePath()); - CssRuleSensor sensor = createCssRuleSensor(rulesExecution); + addInputFile("unknown-rule.css"); sensor.execute(context); + assertThat(context.allIssues()).isEmpty(); assertThat(logTester.logs(LoggerLevel.ERROR)).contains("Unknown stylelint rule or rule not enabled: 'unknown-rule-key'"); } - private static DefaultInputFile createInputFile(SensorContextTester sensorContext, String content, String relativePath) { + private DefaultInputFile addInputFile(String relativePath) { DefaultInputFile inputFile = new TestInputFileBuilder("moduleKey", relativePath) - .setModuleBaseDir(sensorContext.fileSystem().baseDirPath()) + .setModuleBaseDir(context.fileSystem().baseDirPath()) .setType(Type.MAIN) - .setLanguage(CssLanguage.KEY) + .setLanguage(relativePath.split("\\.")[1]) .setCharset(StandardCharsets.UTF_8) - .setContents(content) + .setContents("some css content\n on 2 lines") .build(); - sensorContext.fileSystem().add(inputFile); + context.fileSystem().add(inputFile); return inputFile; } - private CssRuleSensor createCssRuleSensor(LinterCommandProvider commandProvider) { - return new CssRuleSensor(new TestBundleHandler(), checkFactory, commandProvider); - } - - private CssRuleSensor createCssRuleSensor(LinterCommandProvider commandProvider, @Nullable AnalysisWarningsWrapper analysisWarnings) { - return new CssRuleSensor(new TestBundleHandler(), checkFactory, commandProvider, analysisWarnings); - } - - private TestLinterCommandProvider getCommandProvider() { - return new TestLinterCommandProvider().nodeScript("/executables/mockStylelint.js", inputFile.absolutePath()); - } - - private static class TestLinterCommandProvider implements LinterCommandProvider { - - private String[] elements; - - private static String resourceScript(String script) { - try { - return new File(TestLinterCommandProvider.class.getResource(script).toURI()).getAbsolutePath(); - } catch (URISyntaxException e) { - throw new IllegalStateException(e); - } - } - - TestLinterCommandProvider nodeScript(String script, String args) { - this.elements = new String[]{ resourceScript(script), args}; - return this; - } - - @Override - public NodeCommand nodeCommand(File deployDestination, SensorContext context, Consumer<String> output, Consumer<String> error) { - return NodeCommand.builder() - .outputConsumer(output) - .errorConsumer(error) - .minNodeVersion(6) - .configuration(context.config()) - .nodeJsArgs(elements) - .build(); - } - - @Override - public String configPath(File deployDestination) { - return new File(deployDestination, "testconfig.json").getAbsolutePath(); - } - } - - private static class InvalidCommandProvider implements LinterCommandProvider { - - @Override - public NodeCommand nodeCommand(File deployDestination, SensorContext context, Consumer<String> output, Consumer<String> error) { - throw new NodeCommandException("Some problem happened."); - } - - @Override - public String configPath(File deployDestination) { - return new File(deployDestination, "testconfig.json").getAbsolutePath(); - } - } - - private static class TestBundleHandler implements BundleHandler { - @Override - public void deployBundle(File deployDestination) { - // do nothing - } - } - } diff --git a/sonar-css-plugin/src/test/java/org/sonar/css/plugin/CssRulesDefinitionTest.java b/sonar-css-plugin/src/test/java/org/sonar/css/plugin/CssRulesDefinitionTest.java index 8521561..1522c99 100644 --- a/sonar-css-plugin/src/test/java/org/sonar/css/plugin/CssRulesDefinitionTest.java +++ b/sonar-css-plugin/src/test/java/org/sonar/css/plugin/CssRulesDefinitionTest.java @@ -28,7 +28,7 @@ public class CssRulesDefinitionTest { @Test public void test_with_external_rules() { - CssRulesDefinition rulesDefinition = new CssRulesDefinition(true); + CssRulesDefinition rulesDefinition = new CssRulesDefinition(); RulesDefinition.Context context = new RulesDefinition.Context(); rulesDefinition.define(context); @@ -46,19 +46,4 @@ public class CssRulesDefinitionTest { assertThat(mainRepository.isExternal()).isEqualTo(false); assertThat(mainRepository.rules()).hasSize(CssRules.getRuleClasses().size()); } - - @Test - public void test_no_external_rules() throws Exception { - CssRulesDefinition rulesDefinition = new CssRulesDefinition(false); - RulesDefinition.Context context = new RulesDefinition.Context(); - rulesDefinition.define(context); - - assertThat(context.repositories()).hasSize(1); - - RulesDefinition.Repository repository = context.repository("css"); - assertThat(repository.name()).isEqualTo("SonarAnalyzer"); - assertThat(repository.language()).isEqualTo("css"); - assertThat(repository.isExternal()).isEqualTo(false); - assertThat(repository.rules()).hasSize(CssRules.getRuleClasses().size()); - } } diff --git a/sonar-css-plugin/src/test/java/org/sonar/css/plugin/StylelintCommandProviderTest.java b/sonar-css-plugin/src/test/java/org/sonar/css/plugin/StylelintCommandProviderTest.java deleted file mode 100644 index f2d2fed..0000000 --- a/sonar-css-plugin/src/test/java/org/sonar/css/plugin/StylelintCommandProviderTest.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * SonarCSS - * Copyright (C) 2018-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.css.plugin; - -import java.io.File; -import java.util.function.Consumer; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; -import org.sonar.api.batch.sensor.internal.SensorContextTester; -import org.sonar.api.utils.log.LogTester; -import org.sonarsource.nodejs.NodeCommand; - -import static org.assertj.core.api.Assertions.assertThat; - -public class StylelintCommandProviderTest { - - @Rule - public TemporaryFolder temporaryFolder = new TemporaryFolder(); - - @Rule - public final LogTester logTester = new LogTester(); - - @Test - public void test() { - StylelintCommandProvider stylelintCommandProvider = new StylelintCommandProvider(); - File deployDestination = new File("deploy_destination"); - File baseDir = new File("src/test/resources").getAbsoluteFile(); - SensorContextTester context = SensorContextTester.create(baseDir); - context.settings().setProperty(CssPlugin.FILE_SUFFIXES_KEY, ".foo,.bar") - .setProperty("sonar.javascript.file.suffixes", ".js") - .setProperty("sonar.php.file.suffixes", ".php") - .setProperty("sonar.java.file.suffixes", ".java"); - Consumer<String> noop = a -> {}; - NodeCommand nodeCommand = stylelintCommandProvider.nodeCommand(deployDestination, context, noop, noop); - assertThat(nodeCommand.toString()).endsWith( - String.join(" ", - new File(deployDestination, "css-bundle/node_modules/stylelint/bin/stylelint").getAbsolutePath(), - baseDir.getAbsolutePath() + File.separator + "**" + File.separator + "*{.foo,.bar,.php}", - "--config", - new File(deployDestination, "css-bundle/stylelintconfig.json").getAbsolutePath(), - "-f", - "json") - ); - } -} diff --git a/sonar-css-plugin/src/test/java/org/sonar/css/plugin/bundle/CssAnalyzerBundleTest.java b/sonar-css-plugin/src/test/java/org/sonar/css/plugin/bundle/CssAnalyzerBundleTest.java new file mode 100644 index 0000000..dadb16c --- /dev/null +++ b/sonar-css-plugin/src/test/java/org/sonar/css/plugin/bundle/CssAnalyzerBundleTest.java @@ -0,0 +1,85 @@ +/* + * SonarCSS + * Copyright (C) 2018-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.css.plugin.bundle; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.api.utils.internal.JUnitTempFolder; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class CssAnalyzerBundleTest { + + @Rule + public JUnitTempFolder tempFolder = new JUnitTempFolder(); + + @Test + public void default_css_bundle_location() throws Exception { + CssAnalyzerBundle bundle = new CssAnalyzerBundle(); + assertThat(bundle.bundleLocation).isEqualTo("/css-bundle.zip"); + } + + @Test + public void almost_empty_css_bundle() throws Exception { + Bundle bundle = new CssAnalyzerBundle("/bundle/test-css-bundle.zip"); + Path deployLocation = tempFolder.newDir().toPath(); + String expectedStartServer = deployLocation.resolve(Paths.get("css-bundle", "bin", "server")).toString(); + bundle.deploy(deployLocation); + String script = bundle.startServerScript(); + assertThat(script).isEqualTo(expectedStartServer); + File scriptFile = new File(script); + assertThat(scriptFile).exists(); + String content = new String(Files.readAllBytes(scriptFile.toPath()), StandardCharsets.UTF_8); + assertThat(content).startsWith("#!/usr/bin/env node"); + } + + @Test + public void missing_bundle() throws Exception { + Bundle bundle = new CssAnalyzerBundle("/bundle/invalid-bundle-path.zip"); + assertThatThrownBy(() -> bundle.deploy(tempFolder.newDir().toPath())) + .isInstanceOf(IllegalStateException.class) + .hasMessage("css-bundle not found in /bundle/invalid-bundle-path.zip"); + } + + @Test + public void invalid_bundle_zip() throws Exception { + Bundle bundle = new CssAnalyzerBundle("/bundle/invalid-zip-file.zip"); + assertThatThrownBy(() -> bundle.deploy(tempFolder.newDir().toPath())) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Failed to deploy css-bundle (with classpath '/bundle/invalid-zip-file.zip')"); + } + + @Test + public void should_not_fail_when_deployed_twice() throws Exception { + Bundle bundle = new CssAnalyzerBundle("/bundle/test-css-bundle.zip"); + Path deployLocation = tempFolder.newDir().toPath(); + assertThatCode(() -> { + bundle.deploy(deployLocation); + bundle.deploy(deployLocation); + }).doesNotThrowAnyException(); + } +} diff --git a/sonar-css-plugin/src/test/java/org/sonar/css/plugin/bundle/CssBundleHandlerTest.java b/sonar-css-plugin/src/test/java/org/sonar/css/plugin/bundle/CssBundleHandlerTest.java deleted file mode 100644 index f76c912..0000000 --- a/sonar-css-plugin/src/test/java/org/sonar/css/plugin/bundle/CssBundleHandlerTest.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * SonarCSS - * Copyright (C) 2018-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.css.plugin.bundle; - -import java.io.File; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.junit.rules.TemporaryFolder; - -import static org.assertj.core.api.Assertions.assertThat; - -public class CssBundleHandlerTest { - - @Rule - public TemporaryFolder temporaryFolder = new TemporaryFolder(); - - @Rule - public ExpectedException expectedException = ExpectedException.none(); - - private File DEPLOY_DESTINATION; - - @Before - public void setUp() throws Exception { - DEPLOY_DESTINATION = temporaryFolder.newFolder("deployDestination"); - } - - @Test - public void test() { - CssBundleHandler bundleHandler = new CssBundleHandler(); - bundleHandler.bundleLocation = "/bundle/test-bundle.zip"; - bundleHandler.deployBundle(DEPLOY_DESTINATION); - - assertThat(new File(DEPLOY_DESTINATION, "test-bundle.js").exists()).isTrue(); - } - -} diff --git a/sonar-css-plugin/src/test/java/org/sonar/css/plugin/server/CssAnalyzerBridgeServerTest.java b/sonar-css-plugin/src/test/java/org/sonar/css/plugin/server/CssAnalyzerBridgeServerTest.java new file mode 100644 index 0000000..302b6bb --- /dev/null +++ b/sonar-css-plugin/src/test/java/org/sonar/css/plugin/server/CssAnalyzerBridgeServerTest.java @@ -0,0 +1,254 @@ +/* + * SonarCSS + * Copyright (C) 2018-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.css.plugin.server; + +import java.nio.file.Path; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.batch.fs.internal.DefaultInputFile; +import org.sonar.api.batch.fs.internal.TestInputFileBuilder; +import org.sonar.api.batch.sensor.internal.SensorContextTester; +import org.sonar.api.config.internal.MapSettings; +import org.sonar.api.utils.internal.JUnitTempFolder; +import org.sonar.api.utils.log.LogTester; +import org.sonar.css.plugin.bundle.Bundle; +import org.sonar.css.plugin.server.AnalyzerBridgeServer.Issue; +import org.sonar.css.plugin.server.AnalyzerBridgeServer.Request; +import org.sonar.css.plugin.server.exception.ServerAlreadyFailedException; +import org.sonarsource.nodejs.NodeCommand; +import org.sonarsource.nodejs.NodeCommandBuilder; +import org.sonarsource.nodejs.NodeCommandException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.sonar.api.utils.log.LoggerLevel.DEBUG; +import static org.sonar.api.utils.log.LoggerLevel.INFO; +import static org.sonar.api.utils.log.LoggerLevel.WARN; + +public class CssAnalyzerBridgeServerTest { + + private static final String START_SERVER_SCRIPT = "startServer.js"; + private static final String CONFIG_FILE = "config.json"; + private static final int TEST_TIMEOUT_SECONDS = 1; + + @org.junit.Rule + public LogTester logTester = new LogTester(); + + @org.junit.Rule + public final ExpectedException thrown = ExpectedException.none(); + + @org.junit.Rule + public JUnitTempFolder tempFolder = new JUnitTempFolder(); + + private SensorContextTester context; + private CssAnalyzerBridgeServer cssAnalyzerBridgeServer; + + @Before + public void setUp() throws Exception { + context = SensorContextTester.create(tempFolder.newDir()); + context.fileSystem().setWorkDir(tempFolder.newDir().toPath()); + } + + @After + public void tearDown() throws Exception { + if (cssAnalyzerBridgeServer != null) { + cssAnalyzerBridgeServer.clean(); + } + } + + @Test + public void default_timeout() { + CssAnalyzerBridgeServer server = new CssAnalyzerBridgeServer(mock(Bundle.class)); + assertThat(server.timeoutSeconds).isEqualTo(60); + } + + @Test + public void issue_constructor() { + Issue issue = new Issue(2, "r", "t"); + assertThat(issue.line).isEqualTo(2); + assertThat(issue.rule).isEqualTo("r"); + assertThat(issue.text).isEqualTo("t"); + } + + @Test + public void should_throw_when_not_existing_start_script() throws Exception { + cssAnalyzerBridgeServer = createCssAnalyzerBridgeServer("NOT_EXISTING.js"); + + thrown.expect(NodeCommandException.class); + thrown.expectMessage("Node.js script to start css-bundle server doesn't exist"); + + cssAnalyzerBridgeServer.startServer(context); + } + + @Test + public void should_throw_if_failed_to_build_node_command() throws Exception { + NodeCommandBuilder nodeCommandBuilder = mock(NodeCommandBuilder.class, invocation -> { + if (NodeCommandBuilder.class.equals(invocation.getMethod().getReturnType())) { + return invocation.getMock(); + } else { + throw new NodeCommandException("msg"); + } + }); + + cssAnalyzerBridgeServer = new CssAnalyzerBridgeServer(nodeCommandBuilder, TEST_TIMEOUT_SECONDS, new TestBundle(START_SERVER_SCRIPT)); + + thrown.expect(NodeCommandException.class); + thrown.expectMessage("msg"); + + cssAnalyzerBridgeServer.startServerLazily(context); + } + + @Test + public void should_forward_process_streams() throws Exception { + cssAnalyzerBridgeServer = createCssAnalyzerBridgeServer(); + cssAnalyzerBridgeServer.startServerLazily(context); + + assertThat(logTester.logs(DEBUG)).contains("testing debug log"); + assertThat(logTester.logs(WARN)).contains("testing warn log"); + assertThat(logTester.logs(INFO)).contains("testing info log"); + } + + @Test + public void should_get_answer_from_server() throws Exception { + cssAnalyzerBridgeServer = createCssAnalyzerBridgeServer(); + cssAnalyzerBridgeServer.startServerLazily(context); + + Request request = new Request("/absolute/path/file.css", CONFIG_FILE); + Issue[] issues = cssAnalyzerBridgeServer.analyze(request); + assertThat(issues).hasSize(1); + assertThat(issues[0].line).isEqualTo(2); + assertThat(issues[0].rule).isEqualTo("block-no-empty"); + assertThat(issues[0].text).isEqualTo("Unexpected empty block"); + + request = new Request("/absolute/path/empty.css", CONFIG_FILE); + issues = cssAnalyzerBridgeServer.analyze(request); + assertThat(issues).isEmpty(); + } + + @Test + public void should_throw_if_failed_to_start() throws Exception { + cssAnalyzerBridgeServer = createCssAnalyzerBridgeServer("throw.js"); + + thrown.expect(NodeCommandException.class); + thrown.expectMessage("Failed to start server (" + TEST_TIMEOUT_SECONDS + "s timeout)"); + + cssAnalyzerBridgeServer.startServerLazily(context); + } + + @Test + public void should_return_command_info() throws Exception { + cssAnalyzerBridgeServer = createCssAnalyzerBridgeServer(); + assertThat(cssAnalyzerBridgeServer.getCommandInfo()).isEqualTo("Node.js command to start css-bundle server was not built yet."); + + cssAnalyzerBridgeServer.startServerLazily(context); + assertThat(cssAnalyzerBridgeServer.getCommandInfo()).contains("Node.js command to start css-bundle was: ", "node", START_SERVER_SCRIPT); + assertThat(cssAnalyzerBridgeServer.getCommandInfo()).doesNotContain("--max-old-space-size"); + } + + @Test + public void should_set_max_old_space_size() throws Exception { + cssAnalyzerBridgeServer = createCssAnalyzerBridgeServer(); + context.setSettings(new MapSettings().setProperty("sonar.css.node.maxspace", 2048)); + cssAnalyzerBridgeServer.startServerLazily(context); + assertThat(cssAnalyzerBridgeServer.getCommandInfo()).contains("--max-old-space-size=2048"); + } + + @Test + public void test_isAlive() throws Exception { + cssAnalyzerBridgeServer = createCssAnalyzerBridgeServer(); + assertThat(cssAnalyzerBridgeServer.isAlive()).isFalse(); + cssAnalyzerBridgeServer.startServerLazily(context); + assertThat(cssAnalyzerBridgeServer.isAlive()).isTrue(); + cssAnalyzerBridgeServer.stop(); + assertThat(cssAnalyzerBridgeServer.isAlive()).isFalse(); + } + + @Test + public void test_lazy_start() throws Exception { + String alreadyStarted = "css-bundle server is up, no need to start."; + String starting = "Starting Node.js process to start css-bundle server at port"; + cssAnalyzerBridgeServer = createCssAnalyzerBridgeServer(); + cssAnalyzerBridgeServer.startServerLazily(context); + assertThat(logTester.logs(DEBUG).stream().anyMatch(s -> s.startsWith(starting))).isTrue(); + assertThat(logTester.logs(DEBUG)).doesNotContain(alreadyStarted); + logTester.clear(); + cssAnalyzerBridgeServer.startServerLazily(context); + assertThat(logTester.logs(DEBUG).stream().noneMatch(s -> s.startsWith(starting))).isTrue(); + assertThat(logTester.logs(DEBUG)).contains(alreadyStarted); + } + + @Test + public void should_throw_special_exception_when_failed_already() throws Exception { + cssAnalyzerBridgeServer = createCssAnalyzerBridgeServer("throw.js"); + String failedToStartExceptionMessage = "Failed to start server (" + TEST_TIMEOUT_SECONDS + "s timeout)"; + assertThatThrownBy(() -> cssAnalyzerBridgeServer.startServerLazily(context)) + .isInstanceOf(NodeCommandException.class) + .hasMessage(failedToStartExceptionMessage); + + assertThatThrownBy(() -> cssAnalyzerBridgeServer.startServerLazily(context)) + .isInstanceOf(ServerAlreadyFailedException.class); + } + + @Test + public void should_fail_if_bad_json_response() throws Exception { + cssAnalyzerBridgeServer = createCssAnalyzerBridgeServer(START_SERVER_SCRIPT); + cssAnalyzerBridgeServer.deploy(context.fileSystem().workDir()); + cssAnalyzerBridgeServer.startServerLazily(context); + + DefaultInputFile inputFile = TestInputFileBuilder.create("foo", "invalid-json-response.css") + .build(); + Request request = new Request(inputFile.absolutePath(), CONFIG_FILE); + assertThatThrownBy(() -> cssAnalyzerBridgeServer.analyze(request)).isInstanceOf(IllegalStateException.class); + assertThat(context.allIssues()).isEmpty(); + } + + + public static CssAnalyzerBridgeServer createCssAnalyzerBridgeServer(String startServerScript) { + CssAnalyzerBridgeServer server = new CssAnalyzerBridgeServer(NodeCommand.builder(), TEST_TIMEOUT_SECONDS, new TestBundle(startServerScript)); + server.start(); + return server; + } + + public static CssAnalyzerBridgeServer createCssAnalyzerBridgeServer() { + return createCssAnalyzerBridgeServer(START_SERVER_SCRIPT); + } + + static class TestBundle implements Bundle { + + final String startServerScript; + + TestBundle(String startServerScript) { + this.startServerScript = startServerScript; + } + + @Override + public void deploy(Path deployLocation) { + // no-op for unit test + } + + @Override + public String startServerScript() { + return "src/test/resources/mock-start-server/" + startServerScript; + } + } +} diff --git a/sonar-css-plugin/src/test/java/org/sonar/css/plugin/server/NetUtilsTest.java b/sonar-css-plugin/src/test/java/org/sonar/css/plugin/server/NetUtilsTest.java new file mode 100644 index 0000000..0aee145 --- /dev/null +++ b/sonar-css-plugin/src/test/java/org/sonar/css/plugin/server/NetUtilsTest.java @@ -0,0 +1,55 @@ +/* + * SonarCSS + * Copyright (C) 2018-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.css.plugin.server; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; +import org.awaitility.Awaitility; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +public class NetUtilsTest { + + @Test + public void findOpenPort_should_not_return_zero() throws IOException { + assertThat(NetUtils.findOpenPort()) + .isGreaterThan(0) + .isLessThanOrEqualTo(65535); + } + + @Test + public void waitServerToStart_can_be_interrupted() throws InterruptedException { + // try to connect to a port that does not exists + Thread worker = new Thread(() -> NetUtils.waitServerToStart("localhost", 8, 1000)); + worker.start(); + Awaitility.setDefaultTimeout(1, TimeUnit.SECONDS); + // wait for the worker thread to start and to be blocked on Thread.sleep(20); + await().until(() -> worker.getState() == Thread.State.TIMED_WAITING); + + long start = System.currentTimeMillis(); + worker.interrupt(); + worker.join(); + long timeToInterrupt = System.currentTimeMillis() - start; + assertThat(timeToInterrupt).isLessThan(20); + } + +} |
