]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-2657, SONAR-3677 Compute file hash and introduce partial analysis mode
authorJulien HENRY <julien.henry@sonarsource.com>
Tue, 1 Oct 2013 15:43:48 +0000 (17:43 +0200)
committerJulien HENRY <julien.henry@sonarsource.com>
Tue, 1 Oct 2013 17:13:05 +0000 (19:13 +0200)
14 files changed:
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/batch/PartialScanFilter.java [new file with mode: 0644]
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/sensors/FileHashSensor.java [new file with mode: 0644]
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/utils/HashBuilder.java [new file with mode: 0644]
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/utils/package-info.java [new file with mode: 0644]
plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/batch/PartialScanFilterTest.java [new file with mode: 0644]
plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/sensors/FileHashSensorTest.java [new file with mode: 0644]
plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/utils/HashBuilderTest.java [new file with mode: 0644]
sonar-core/src/main/java/org/sonar/core/persistence/DryRunDatabaseFactory.java
sonar-core/src/main/java/org/sonar/core/source/SnapshotDataType.java
sonar-core/src/test/java/org/sonar/core/persistence/DryRunDatabaseFactoryTest.java
sonar-core/src/test/resources/org/sonar/core/persistence/DryRunDatabaseFactoryTest/multi-modules-with-issues.xml
sonar-plugin-api/src/main/java/org/sonar/api/CoreProperties.java
sonar-plugin-api/src/main/java/org/sonar/api/scan/filesystem/FileSystemFilter.java

index ac4dbd8b359c339afe801c47b779bb87196bc7c2..4fcf1dc68b3b23a94948b621a1840ff6f6311ad0 100644 (file)
@@ -19,6 +19,9 @@
  */
 package org.sonar.plugins.core;
 
+import org.sonar.plugins.core.batch.PartialScanFilter;
+
+import org.sonar.plugins.core.utils.HashBuilder;
 import com.google.common.collect.ImmutableList;
 import org.sonar.api.*;
 import org.sonar.api.checks.NoSonarFilter;
@@ -311,6 +314,9 @@ public final class CorePlugin extends SonarPlugin {
         FilesDecorator.class,
         IndexProjectPostJob.class,
         ManualMeasureDecorator.class,
+        HashBuilder.class,
+        FileHashSensor.class,
+        PartialScanFilter.class,
 
         // time machine
         TendencyDecorator.class,
diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/batch/PartialScanFilter.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/batch/PartialScanFilter.java
new file mode 100644 (file)
index 0000000..903a5ca
--- /dev/null
@@ -0,0 +1,126 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2013 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.plugins.core.batch;
+
+import com.google.common.collect.Maps;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang.StringUtils;
+import org.picocontainer.Startable;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.batch.bootstrap.ProjectDefinition;
+import org.sonar.api.config.Settings;
+import org.sonar.api.database.model.Snapshot;
+import org.sonar.api.scan.filesystem.FileSystemFilter;
+import org.sonar.api.scan.filesystem.PathResolver;
+import org.sonar.api.utils.SonarException;
+import org.sonar.batch.components.PastSnapshot;
+import org.sonar.batch.components.PastSnapshotFinder;
+import org.sonar.core.source.SnapshotDataType;
+import org.sonar.core.source.jdbc.SnapshotDataDao;
+import org.sonar.core.source.jdbc.SnapshotDataDto;
+import org.sonar.plugins.core.utils.HashBuilder;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * When enabled this filter will only allow modified files to be analyzed.
+ * @since 4.0
+ */
+public class PartialScanFilter implements FileSystemFilter, Startable {
+
+  private PathResolver pathResolver;
+  private HashBuilder hashBuilder;
+  private Settings settings;
+  private SnapshotDataDao snapshotDataDao;
+  private PastSnapshotFinder pastSnapshotFinder;
+  private Snapshot snapshot;
+  private ProjectDefinition module;
+
+  private Map<String, String> fileHashMap = Maps.newHashMap();
+
+  public PartialScanFilter(Settings settings, ProjectDefinition module, PathResolver pathResolver, HashBuilder hashBuilder,
+    Snapshot snapshot,
+    SnapshotDataDao snapshotDataDao,
+    PastSnapshotFinder pastSnapshotFinder) {
+    this.settings = settings;
+    this.module = module;
+    this.pathResolver = pathResolver;
+    this.hashBuilder = hashBuilder;
+    this.snapshot = snapshot;
+    this.snapshotDataDao = snapshotDataDao;
+    this.pastSnapshotFinder = pastSnapshotFinder;
+  }
+
+  @Override
+  public void start() {
+    // Extract previous checksum of all files of this module and store
+    // them in a map
+    if (settings.getBoolean(CoreProperties.PARTIAL_ANALYSIS)) {
+      if (!settings.getBoolean(CoreProperties.DRY_RUN)) {
+        throw new SonarException("Partial analysis is only supported with dry run mode");
+      }
+      PastSnapshot pastSnapshot = pastSnapshotFinder.findPreviousAnalysis(snapshot);
+      if (pastSnapshot.isRelatedToSnapshot()) {
+        Collection<SnapshotDataDto> selectSnapshotData = snapshotDataDao.selectSnapshotData(pastSnapshot.getProjectSnapshot().getId().longValue(),
+          Arrays.asList(SnapshotDataType.FILE_HASH.getValue()));
+        if (!selectSnapshotData.isEmpty()) {
+          SnapshotDataDto snapshotDataDto = selectSnapshotData.iterator().next();
+          String data = snapshotDataDto.getData();
+          try {
+            List<String> lines = IOUtils.readLines(new StringReader(data));
+            for (String line : lines) {
+              String[] keyValue = StringUtils.split(line, "=");
+              if (keyValue.length == 2) {
+                fileHashMap.put(keyValue[0], keyValue[1]);
+              }
+            }
+          } catch (IOException e) {
+            throw new SonarException("Unable to read previous file hashes", e);
+          }
+        }
+      }
+    }
+  }
+
+  @Override
+  public void stop() {
+  }
+
+  @Override
+  public boolean accept(File file, Context context) {
+    if (!settings.getBoolean(CoreProperties.PARTIAL_ANALYSIS)) {
+      return true;
+    }
+    String relativePath = pathResolver.relativePath(module.getBaseDir(), file);
+    String previousHash = fileHashMap.get(relativePath);
+    if (previousHash == null) {
+      return true;
+    }
+    String currentHash = hashBuilder.computeHash(file);
+    return !currentHash.equals(previousHash);
+  }
+
+}
diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/sensors/FileHashSensor.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/sensors/FileHashSensor.java
new file mode 100644 (file)
index 0000000..8e3f8d0
--- /dev/null
@@ -0,0 +1,81 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2013 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.plugins.core.sensors;
+
+import org.sonar.api.batch.Sensor;
+import org.sonar.api.batch.SensorContext;
+import org.sonar.api.resources.Project;
+import org.sonar.api.scan.filesystem.FileQuery;
+import org.sonar.api.scan.filesystem.FileType;
+import org.sonar.api.scan.filesystem.ModuleFileSystem;
+import org.sonar.api.scan.filesystem.PathResolver;
+import org.sonar.batch.index.ComponentDataCache;
+import org.sonar.core.source.SnapshotDataType;
+import org.sonar.plugins.core.utils.HashBuilder;
+
+import java.io.File;
+import java.util.List;
+
+/**
+ * This sensor will compute md5 checksum of each file of the current module and store it in DB
+ * in order to compare it during next analysis and see if the file was modified.
+ * This is used by the partial analysis mode.
+ * @see org.sonar.plugins.core.batch.PartialScanFilter
+ * @since 4.0
+ */
+public final class FileHashSensor implements Sensor {
+
+  private ModuleFileSystem moduleFileSystem;
+  private PathResolver pathResolver;
+  private HashBuilder hashBuilder;
+  private ComponentDataCache componentDataCache;
+
+  public FileHashSensor(ModuleFileSystem moduleFileSystem, PathResolver pathResolver, HashBuilder hashBuilder, ComponentDataCache componentDataCache) {
+    this.moduleFileSystem = moduleFileSystem;
+    this.pathResolver = pathResolver;
+    this.hashBuilder = hashBuilder;
+    this.componentDataCache = componentDataCache;
+  }
+
+  public boolean shouldExecuteOnProject(Project project) {
+    return true;
+  }
+
+  @Override
+  public void analyse(Project project, SensorContext context) {
+    StringBuilder fileHashMap = new StringBuilder();
+    analyse(fileHashMap, project, FileType.SOURCE);
+    analyse(fileHashMap, project, FileType.TEST);
+    componentDataCache.setStringData(project.getKey(), SnapshotDataType.FILE_HASH.getValue(), fileHashMap.toString());
+  }
+
+  private void analyse(StringBuilder fileHashMap, Project project, FileType fileType) {
+    List<File> files = moduleFileSystem.files(FileQuery.on(fileType).onLanguage(project.getLanguageKey()));
+    for (File file : files) {
+      String md5 = hashBuilder.computeHash(file);
+      fileHashMap.append(pathResolver.relativePath(moduleFileSystem.baseDir(), file)).append("=").append(md5).append("\n");
+    }
+  }
+
+  @Override
+  public String toString() {
+    return getClass().getSimpleName();
+  }
+}
diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/utils/HashBuilder.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/utils/HashBuilder.java
new file mode 100644 (file)
index 0000000..87b78a1
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2013 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.plugins.core.utils;
+
+import org.apache.commons.io.IOUtils;
+import org.sonar.api.BatchExtension;
+import org.sonar.api.utils.SonarException;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+
+/**
+ * @since 4.0
+ */
+public final class HashBuilder implements BatchExtension {
+
+  public String computeHash(File file) {
+    FileInputStream fis = null;
+    try {
+      fis = new FileInputStream(file);
+      return org.apache.commons.codec.digest.DigestUtils.md5Hex(fis);
+    } catch (IOException e) {
+      throw new SonarException("Unable to compute file hash", e);
+    } finally {
+      IOUtils.closeQuietly(fis);
+    }
+  }
+}
diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/utils/package-info.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/utils/package-info.java
new file mode 100644 (file)
index 0000000..6d0f75f
--- /dev/null
@@ -0,0 +1,23 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2013 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.plugins.core.utils;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/batch/PartialScanFilterTest.java b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/batch/PartialScanFilterTest.java
new file mode 100644 (file)
index 0000000..7cd7849
--- /dev/null
@@ -0,0 +1,161 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2013 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.plugins.core.batch;
+
+import org.apache.commons.io.FileUtils;
+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.CoreProperties;
+import org.sonar.api.batch.bootstrap.ProjectDefinition;
+import org.sonar.api.config.Settings;
+import org.sonar.api.database.model.Snapshot;
+import org.sonar.api.scan.filesystem.FileSystemFilter;
+import org.sonar.api.scan.filesystem.PathResolver;
+import org.sonar.api.utils.SonarException;
+import org.sonar.batch.components.PastSnapshot;
+import org.sonar.batch.components.PastSnapshotFinder;
+import org.sonar.core.source.SnapshotDataType;
+import org.sonar.core.source.jdbc.SnapshotDataDao;
+import org.sonar.core.source.jdbc.SnapshotDataDto;
+import org.sonar.plugins.core.utils.HashBuilder;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.Date;
+
+import static org.fest.assertions.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class PartialScanFilterTest {
+
+  @Rule
+  public TemporaryFolder temp = new TemporaryFolder();
+
+  @Rule
+  public ExpectedException thrown = ExpectedException.none();
+
+  private PartialScanFilter filter;
+
+  private Settings settings;
+
+  private PastSnapshotFinder pastSnapshotFinder;
+
+  private Snapshot snapshot;
+
+  private File baseDir;
+
+  private SnapshotDataDao snapshotDataDao;
+
+  @Before
+  public void prepare() throws Exception {
+    settings = new Settings();
+    pastSnapshotFinder = mock(PastSnapshotFinder.class);
+    snapshot = mock(Snapshot.class);
+    baseDir = temp.newFolder();
+    snapshotDataDao = mock(SnapshotDataDao.class);
+    filter = new PartialScanFilter(settings, ProjectDefinition.create().setBaseDir(baseDir), new PathResolver(), new HashBuilder(), snapshot,
+      snapshotDataDao, pastSnapshotFinder);
+  }
+
+  @Test
+  public void should_not_run_by_default() throws Exception {
+    filter.start();
+    assertThat(filter.accept(temp.newFile(), mock(FileSystemFilter.Context.class))).isTrue();
+  }
+
+  @Test
+  public void should_throw_if_partial_mode_and_not_in_dryrun() throws Exception {
+    settings.setProperty(CoreProperties.PARTIAL_ANALYSIS, true);
+
+    thrown.expect(SonarException.class);
+    thrown.expectMessage("Partial analysis is only supported with dry run mode");
+    filter.start();
+  }
+
+  @Test
+  public void should_include_if_no_previous_snapshot() throws Exception {
+    settings.setProperty(CoreProperties.PARTIAL_ANALYSIS, true);
+    settings.setProperty(CoreProperties.DRY_RUN, true);
+
+    when(pastSnapshotFinder.findPreviousAnalysis(snapshot)).thenReturn(new PastSnapshot("foo"));
+
+    filter.start();
+    assertThat(filter.accept(new File(baseDir, "src/main/java/foo/Bar.java"), mock(FileSystemFilter.Context.class))).isTrue();
+  }
+
+  @Test
+  public void should_include_if_no_previous_snapshot_data() throws Exception {
+    settings.setProperty(CoreProperties.PARTIAL_ANALYSIS, true);
+    settings.setProperty(CoreProperties.DRY_RUN, true);
+
+    Snapshot previousSnapshot = mock(Snapshot.class);
+    PastSnapshot pastSnapshot = new PastSnapshot("foo", new Date(), previousSnapshot);
+    when(pastSnapshotFinder.findPreviousAnalysis(snapshot)).thenReturn(pastSnapshot);
+
+    filter.start();
+    assertThat(filter.accept(new File(baseDir, "src/main/java/foo/Bar.java"), mock(FileSystemFilter.Context.class))).isTrue();
+  }
+
+  @Test
+  public void should_include_if_different_hash() throws Exception {
+    settings.setProperty(CoreProperties.PARTIAL_ANALYSIS, true);
+    settings.setProperty(CoreProperties.DRY_RUN, true);
+
+    Snapshot previousSnapshot = mock(Snapshot.class);
+    when(previousSnapshot.getId()).thenReturn(123);
+    PastSnapshot pastSnapshot = new PastSnapshot("foo", new Date(), previousSnapshot);
+    when(pastSnapshotFinder.findPreviousAnalysis(snapshot)).thenReturn(pastSnapshot);
+
+    SnapshotDataDto snapshotDataDto = new SnapshotDataDto();
+    snapshotDataDto.setData("src/main/java/foo/Bar.java=abcd1234\n");
+    when(snapshotDataDao.selectSnapshotData(123, Arrays.asList(SnapshotDataType.FILE_HASH.getValue())))
+      .thenReturn(Arrays.asList(snapshotDataDto));
+
+    filter.start();
+    File file = new File(baseDir, "src/main/java/foo/Bar.java");
+    FileUtils.write(file, "foo");
+    assertThat(filter.accept(file, mock(FileSystemFilter.Context.class))).isTrue();
+  }
+
+  @Test
+  public void should_exclude_if_same_hash() throws Exception {
+    settings.setProperty(CoreProperties.PARTIAL_ANALYSIS, true);
+    settings.setProperty(CoreProperties.DRY_RUN, true);
+
+    Snapshot previousSnapshot = mock(Snapshot.class);
+    when(previousSnapshot.getId()).thenReturn(123);
+    PastSnapshot pastSnapshot = new PastSnapshot("foo", new Date(), previousSnapshot);
+    when(pastSnapshotFinder.findPreviousAnalysis(snapshot)).thenReturn(pastSnapshot);
+
+    SnapshotDataDto snapshotDataDto = new SnapshotDataDto();
+    snapshotDataDto.setData("src/main/java/foo/Bar.java=acbd18db4cc2f85cedef654fccc4a4d8\n");
+    when(snapshotDataDao.selectSnapshotData(123, Arrays.asList(SnapshotDataType.FILE_HASH.getValue())))
+      .thenReturn(Arrays.asList(snapshotDataDto));
+
+    filter.start();
+    File file = new File(baseDir, "src/main/java/foo/Bar.java");
+    FileUtils.write(file, "foo");
+    assertThat(filter.accept(file, mock(FileSystemFilter.Context.class))).isFalse();
+  }
+}
diff --git a/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/sensors/FileHashSensorTest.java b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/sensors/FileHashSensorTest.java
new file mode 100644 (file)
index 0000000..0f3d59d
--- /dev/null
@@ -0,0 +1,96 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2013 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.plugins.core.sensors;
+
+import org.apache.commons.configuration.PropertiesConfiguration;
+import org.apache.commons.io.FileUtils;
+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.SensorContext;
+import org.sonar.api.resources.Java;
+import org.sonar.api.resources.Project;
+import org.sonar.api.scan.filesystem.FileQuery;
+import org.sonar.api.scan.filesystem.ModuleFileSystem;
+import org.sonar.api.scan.filesystem.PathResolver;
+import org.sonar.batch.index.ComponentDataCache;
+import org.sonar.plugins.core.utils.HashBuilder;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.fest.assertions.Assertions.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class FileHashSensorTest {
+
+  @Rule
+  public TemporaryFolder temp = new TemporaryFolder();
+
+  private FileHashSensor sensor;
+
+  @Rule
+  public ExpectedException thrown = ExpectedException.none();
+
+  private ModuleFileSystem fileSystem;
+
+  private ComponentDataCache componentDataCache;
+
+  private Project project;
+
+  @Before
+  public void prepare() {
+    fileSystem = mock(ModuleFileSystem.class);
+    componentDataCache = mock(ComponentDataCache.class);
+    sensor = new FileHashSensor(fileSystem, new PathResolver(), new HashBuilder(), componentDataCache);
+    PropertiesConfiguration conf = new PropertiesConfiguration();
+    conf.setProperty("sonar.language", "java");
+    project = new Project("java_project").setConfiguration(conf).setLanguage(Java.INSTANCE);
+  }
+
+  @Test
+  public void improve_code_coverage() throws Exception {
+    assertThat(sensor.shouldExecuteOnProject(project)).isTrue();
+    assertThat(sensor.toString()).isEqualTo("FileHashSensor");
+  }
+
+  @Test
+  public void computeHashes() throws Exception {
+    File baseDir = temp.newFolder();
+    File file1 = new File(baseDir, "src/com/foo/Bar.java");
+    FileUtils.write(file1, "Bar");
+    File file2 = new File(baseDir, "src/com/foo/Foo.java");
+    FileUtils.write(file2, "Foo");
+    when(fileSystem.baseDir()).thenReturn(baseDir);
+    when(fileSystem.files(any(FileQuery.class))).thenReturn(Arrays.asList(file1, file2)).thenReturn(Collections.<File> emptyList());
+    sensor.analyse(project, mock(SensorContext.class));
+
+    verify(componentDataCache).setStringData("java_project", "hash",
+      "src/com/foo/Bar.java=ddc35f88fa71b6ef142ae61f35364653\n"
+        + "src/com/foo/Foo.java=1356c67d7ad1638d816bfb822dd2c25d\n");
+  }
+
+}
diff --git a/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/utils/HashBuilderTest.java b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/utils/HashBuilderTest.java
new file mode 100644 (file)
index 0000000..a0819b5
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2013 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.plugins.core.utils;
+
+import org.apache.commons.io.FileUtils;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.api.utils.SonarException;
+
+import java.io.File;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+public class HashBuilderTest {
+
+  @Rule
+  public TemporaryFolder temp = new TemporaryFolder();
+
+  @Test
+  public void should_compute_hash() throws Exception {
+    File tempFile = temp.newFile();
+    FileUtils.write(tempFile, "foobar");
+    assertThat(new HashBuilder().computeHash(tempFile)).isEqualTo("3858f62230ac3c915f300c664312c63f");
+  }
+
+  @Test(expected = SonarException.class)
+  public void should_throw_on_not_existing_file() throws Exception {
+    File tempFolder = temp.newFolder();
+    new HashBuilder().computeHash(new File(tempFolder, "unknowFile.txt"));
+  }
+}
index 1d52f4dd296f56ae211afa4674e00190f0c59225..6a7a351ce9cdc4ab22dc84ef2bd9a82e8626d6aa 100644 (file)
@@ -25,6 +25,7 @@ import org.slf4j.LoggerFactory;
 import org.sonar.api.ServerComponent;
 import org.sonar.api.issue.Issue;
 import org.sonar.api.utils.SonarException;
+import org.sonar.core.source.SnapshotDataType;
 
 import javax.annotation.Nullable;
 import javax.sql.DataSource;
@@ -100,8 +101,37 @@ public class DryRunDatabaseFactory implements ServerComponent {
 
       template.copyTable(source, dest, "events", "SELECT * FROM events WHERE resource_id=" + projectId);
 
-      template.copyTable(source, dest, "snapshots", "SELECT * FROM snapshots WHERE project_id=" + projectId);
-      template.copyTable(source, dest, "project_measures", "SELECT m.* FROM project_measures m INNER JOIN snapshots s on m.snapshot_id=s.id WHERE s.project_id=" + projectId);
+      StringBuilder snapshotQuery = new StringBuilder()
+        // All snapshots of root_project for alerts on differential periods
+        .append("SELECT * FROM snapshots WHERE project_id=")
+        .append(projectId)
+        // Plus all last snapshots of all modules having hash data for partial analysis
+        .append(" UNION SELECT snap.* FROM snapshots snap")
+        .append(" INNER JOIN (")
+        .append(projectQuery(projectId, true))
+        .append(") res")
+        .append(" ON snap.project_id=res.id")
+        .append(" INNER JOIN snapshot_data data")
+        .append(" ON snap.id=data.snapshot_id")
+        .append(" AND data.data_type='").append(SnapshotDataType.FILE_HASH.getValue()).append("'")
+        .append(" AND snap.islast=").append(database.getDialect().getTrueSqlValue());
+      template.copyTable(source, dest, "snapshots", snapshotQuery.toString());
+
+      StringBuilder snapshotDataQuery = new StringBuilder()
+        .append("SELECT data.* FROM snapshot_data data")
+        .append(" INNER JOIN snapshots s")
+        .append(" ON s.id=data.snapshot_id")
+        .append(" AND s.islast=").append(database.getDialect().getTrueSqlValue())
+        .append(" INNER JOIN (")
+        .append(projectQuery(projectId, true))
+        .append(") res")
+        .append(" ON data.resource_id=res.id")
+        .append(" AND data.data_type='").append(SnapshotDataType.FILE_HASH.getValue()).append("'");
+      template.copyTable(source, dest, "snapshot_data", snapshotDataQuery.toString());
+
+      // All measures of snapshots of root project for alerts on differential periods
+      template.copyTable(source, dest, "project_measures", "SELECT m.* FROM project_measures m INNER JOIN snapshots s on m.snapshot_id=s.id "
+        + "WHERE s.project_id=" + projectId);
 
       StringBuilder issueQuery = new StringBuilder()
         .append("SELECT issues.* FROM issues")
index 577d356b6911b1fa46ce5051d99de7fef4cf2bf6..b45eb9d32affdca4d58aeebd619532ed5a63c34e 100644 (file)
@@ -23,7 +23,8 @@ package org.sonar.core.source;
 public enum SnapshotDataType {
 
   SYNTAX_HIGHLIGHTING("highlight_syntax"),
-  SYMBOL_HIGHLIGHTING("symbol");
+  SYMBOL_HIGHLIGHTING("symbol"),
+  FILE_HASH("hash");
 
   private SnapshotDataType(String value) {
     this.value = value;
index a724df04af43442d07b0594baabd284b58505228..79c5461b89264428d6dbfe165c6862c96ef851de 100644 (file)
@@ -103,8 +103,9 @@ public class DryRunDatabaseFactoryTest extends AbstractDaoTestCase {
     dataSource = createDatabase(database);
     assertThat(rowCount("issues")).isEqualTo(1);
     assertThat(rowCount("projects")).isEqualTo(4);
-    assertThat(rowCount("snapshots")).isEqualTo(1);
-    assertThat(rowCount("project_measures")).isEqualTo(2);
+    assertThat(rowCount("snapshots")).isEqualTo(4);
+    assertThat(rowCount("snapshot_data")).isEqualTo(2);
+    assertThat(rowCount("project_measures")).isEqualTo(4);
   }
 
   @Test
@@ -116,8 +117,8 @@ public class DryRunDatabaseFactoryTest extends AbstractDaoTestCase {
     dataSource = createDatabase(database);
     assertThat(rowCount("issues")).isEqualTo(1);
     assertThat(rowCount("projects")).isEqualTo(2);
-    assertThat(rowCount("snapshots")).isEqualTo(1);
-    assertThat(rowCount("project_measures")).isEqualTo(2);
+    assertThat(rowCount("snapshots")).isEqualTo(2);
+    assertThat(rowCount("project_measures")).isEqualTo(4);
   }
 
   @Test
index af4cd1595c52f425ac44d3e5c754b18f14a05518..f7e364c0ec927093b252552dfe156e0984b7c493 100644 (file)
   <snapshots id="3002" project_id="302" root_project_id="300" root_snapshot_id="3000" path="3000." islast="[true]"/>
   <snapshots id="3003" project_id="303" root_project_id="300" root_snapshot_id="3000" path="3000.3001." islast="[true]"/>
 
+  <snapshots id="3010" project_id="300" root_project_id="300" root_snapshot_id="[null]" path="" islast="[false]"/>
+  <snapshots id="3011" project_id="301" root_project_id="300" root_snapshot_id="3010" path="3010." islast="[false]"/>
+  <snapshots id="3012" project_id="302" root_project_id="300" root_snapshot_id="3010" path="3010." islast="[false]"/>
+  <snapshots id="3013" project_id="303" root_project_id="300" root_snapshot_id="3010" path="3010.3011." islast="[false]"/>
+
+  <snapshot_data id="1" snapshot_id="3001" resource_id="301" snapshot_data="foo=AB12" data_type="hash" />
+  <snapshot_data id="2" snapshot_id="3001" resource_id="301" snapshot_data="bar" data_type="other" />
+  <snapshot_data id="3" snapshot_id="3002" resource_id="302" snapshot_data="bar=DC12" data_type="hash" />
+
+  <snapshot_data id="4" snapshot_id="3011" resource_id="301" snapshot_data="foo=CD34" data_type="hash" />
+  <snapshot_data id="5" snapshot_id="3012" resource_id="302" snapshot_data="bar=EF78" data_type="hash" />
+
   <project_measures id="1" value="12" metric_id="1" snapshot_id="3000" />
   <project_measures id="2" value="5" metric_id="1" snapshot_id="3001" />
   <project_measures id="3" value="7" metric_id="1" snapshot_id="3002" />
   <project_measures id="4" value="5" metric_id="1" snapshot_id="3003" />
+
   <project_measures id="5" value="35" metric_id="2" snapshot_id="3000" />
   <project_measures id="6" value="20" metric_id="2" snapshot_id="3001" />
   <project_measures id="7" value="30" metric_id="2" snapshot_id="3002" />
   <project_measures id="8" value="20" metric_id="2" snapshot_id="3003" />
 
+  <project_measures id="11" value="112" metric_id="1" snapshot_id="3010" />
+  <project_measures id="12" value="15" metric_id="1" snapshot_id="3011" />
+  <project_measures id="13" value="17" metric_id="1" snapshot_id="3012" />
+  <project_measures id="14" value="15" metric_id="1" snapshot_id="3013" />
+
+  <project_measures id="15" value="135" metric_id="2" snapshot_id="3010" />
+  <project_measures id="16" value="120" metric_id="2" snapshot_id="3011" />
+  <project_measures id="17" value="130" metric_id="2" snapshot_id="3012" />
+  <project_measures id="18" value="120" metric_id="2" snapshot_id="3013" />
+
 
   <rules id="500" plugin_rule_key="AvoidCycle" plugin_name="squid"/>
   <rules id="501" plugin_rule_key="NullRef" plugin_name="squid"/>
index 9aa3b74b85bf5d3b8ffa3b1dd9708f58aad456a5..f654f1b0d5742f6d7b9f54c7979281865baf8625 100644 (file)
@@ -126,7 +126,6 @@ public interface CoreProperties {
    */
   String CATEGORY_TECHNICAL_DEBT = "technicalDebt";
 
-
   /* Global settings */
   String SONAR_HOME = "SONAR_HOME";
   String PROJECT_BRANCH_PROPERTY = "sonar.branch";
@@ -470,4 +469,9 @@ public interface CoreProperties {
    * @since 4.0
    */
   String CORE_PREVENT_AUTOMATIC_PROJECT_CREATION = "sonar.preventAutoProjectCreation";
+
+  /**
+   * @since 4.0
+   */
+  String PARTIAL_ANALYSIS = "sonar.partialAnalysis";
 }
index 4fb17dcebf42bfb3d67065683554f07849ebc1ec..7d03441df56132ef3bd382c9ea0a802a33569fcd 100644 (file)
@@ -36,7 +36,7 @@ public interface FileSystemFilter extends BatchExtension {
   /**
    * Plugins must not implement this interface. It is provided at runtime.
    */
-  interface Context {
+  public interface Context {
     ModuleFileSystem fileSystem();
 
     FileType type();