diff options
author | Sébastien Lesaint <sebastien.lesaint@sonarsource.com> | 2015-10-09 17:21:49 +0200 |
---|---|---|
committer | Sébastien Lesaint <sebastien.lesaint@sonarsource.com> | 2015-10-14 11:50:47 +0200 |
commit | 844fb1f476fa55d9c1d6b0f49cb86f879d2abb85 (patch) | |
tree | 6ab0feca7cc13caf0b4c3f32672951f36146d387 | |
parent | 1488cd4d3952b65aae551a48814c6feeaad21548 (diff) | |
download | sonarqube-844fb1f476fa55d9c1d6b0f49cb86f879d2abb85.tar.gz sonarqube-844fb1f476fa55d9c1d6b0f49cb86f879d2abb85.zip |
SONAR-6397 read changeset from DB only if File is unmodified
to achieve that, we compare the source hash stored in DB with hash of source in analysis report
this also remove duplication of line hashing algorithm between core and Computation Engine and isolate source and line hashing into specific classes
19 files changed, 751 insertions, 109 deletions
diff --git a/server/sonar-server-benchmarks/src/test/java/org/sonar/server/benchmark/PersistFileSourcesStepTest.java b/server/sonar-server-benchmarks/src/test/java/org/sonar/server/benchmark/PersistFileSourcesStepTest.java index 09520d5b788..0eb79223ef5 100644 --- a/server/sonar-server-benchmarks/src/test/java/org/sonar/server/benchmark/PersistFileSourcesStepTest.java +++ b/server/sonar-server-benchmarks/src/test/java/org/sonar/server/benchmark/PersistFileSourcesStepTest.java @@ -44,9 +44,9 @@ import org.sonar.server.computation.batch.TreeRootHolderRule; import org.sonar.server.computation.component.Component; import org.sonar.server.computation.component.ReportComponent; import org.sonar.server.computation.scm.ScmInfoRepositoryImpl; +import org.sonar.server.computation.source.SourceHashRepositoryImpl; import org.sonar.server.computation.source.SourceLinesRepositoryImpl; import org.sonar.server.computation.step.PersistFileSourcesStep; -import org.sonar.server.source.SourceService; import static org.assertj.core.api.Assertions.assertThat; @@ -88,11 +88,11 @@ public class PersistFileSourcesStepTest { BatchReportDirectoryHolderImpl batchReportDirectoryHolder = new BatchReportDirectoryHolderImpl(); batchReportDirectoryHolder.setDirectory(reportDir); org.sonar.server.computation.batch.BatchReportReader batchReportReader = new BatchReportReaderImpl(batchReportDirectoryHolder); - SourceService sourceService = new SourceService(dbClient, null); analysisMetadataHolder.setIsFirstAnalysis(false); - ScmInfoRepositoryImpl scmInfoRepository = new ScmInfoRepositoryImpl(batchReportReader, analysisMetadataHolder, dbClient, sourceService); - PersistFileSourcesStep step = new PersistFileSourcesStep(dbClient, System2.INSTANCE, treeRootHolder, batchReportReader, - new SourceLinesRepositoryImpl(batchReportReader), scmInfoRepository); + SourceLinesRepositoryImpl sourceLinesRepository = new SourceLinesRepositoryImpl(batchReportReader); + SourceHashRepositoryImpl sourceHashRepository = new SourceHashRepositoryImpl(sourceLinesRepository); + ScmInfoRepositoryImpl scmInfoRepository = new ScmInfoRepositoryImpl(batchReportReader, analysisMetadataHolder, dbClient, sourceHashRepository); + PersistFileSourcesStep step = new PersistFileSourcesStep(dbClient, System2.INSTANCE, treeRootHolder, batchReportReader, sourceLinesRepository, scmInfoRepository); step.execute(); long end = System.currentTimeMillis(); diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/component/FileAttributes.java b/server/sonar-server/src/main/java/org/sonar/server/computation/component/FileAttributes.java index bbcf22e0c08..6e8dc69e4f7 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/computation/component/FileAttributes.java +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/component/FileAttributes.java @@ -29,6 +29,7 @@ import javax.annotation.concurrent.Immutable; @Immutable public class FileAttributes { private final boolean unitTest; + @CheckForNull private final String languageKey; public FileAttributes(boolean unitTest, @Nullable String languageKey) { diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/container/ReportComputeEngineContainerPopulator.java b/server/sonar-server/src/main/java/org/sonar/server/computation/container/ReportComputeEngineContainerPopulator.java index a7f561a5552..1273a1e2d4e 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/computation/container/ReportComputeEngineContainerPopulator.java +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/container/ReportComputeEngineContainerPopulator.java @@ -77,6 +77,7 @@ import org.sonar.server.computation.qualityprofile.ActiveRulesHolderImpl; import org.sonar.server.computation.queue.CeTask; import org.sonar.server.computation.scm.ScmInfoRepositoryImpl; import org.sonar.server.computation.source.LastCommitVisitor; +import org.sonar.server.computation.source.SourceHashRepositoryImpl; import org.sonar.server.computation.source.SourceLinesRepositoryImpl; import org.sonar.server.computation.sqale.SqaleMeasuresVisitor; import org.sonar.server.computation.sqale.SqaleNewMeasuresVisitor; @@ -135,6 +136,7 @@ public final class ReportComputeEngineContainerPopulator implements ContainerPop QualityGateServiceImpl.class, EvaluationResultTextConverterImpl.class, SourceLinesRepositoryImpl.class, + SourceHashRepositoryImpl.class, ScmInfoRepositoryImpl.class, // issues diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/issue/TrackerRawInputFactory.java b/server/sonar-server/src/main/java/org/sonar/server/computation/issue/TrackerRawInputFactory.java index d9c32c5d466..2d670a756a4 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/computation/issue/TrackerRawInputFactory.java +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/issue/TrackerRawInputFactory.java @@ -24,7 +24,6 @@ import java.util.Collections; import java.util.List; import org.sonar.api.issue.Issue; import org.sonar.api.rule.RuleKey; -import org.sonar.api.utils.KeyValueFormat; import org.sonar.api.utils.log.Loggers; import org.sonar.batch.protocol.output.BatchReport; import org.sonar.core.issue.DefaultIssue; @@ -71,7 +70,7 @@ public class TrackerRawInputFactory { @Override protected LineHashSequence loadLineHashSequence() { - Iterable<String> lines; + List<String> lines; if (component.getType() == Component.Type.FILE) { lines = newArrayList(sourceLinesRepository.readLines(component)); } else { diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/scm/ScmInfoRepositoryImpl.java b/server/sonar-server/src/main/java/org/sonar/server/computation/scm/ScmInfoRepositoryImpl.java index 5733265a838..cb4a160a26a 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/computation/scm/ScmInfoRepositoryImpl.java +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/scm/ScmInfoRepositoryImpl.java @@ -27,11 +27,11 @@ import org.sonar.api.utils.log.Loggers; import org.sonar.batch.protocol.output.BatchReport; import org.sonar.db.DbClient; import org.sonar.db.DbSession; -import org.sonar.db.protobuf.DbFileSources; +import org.sonar.db.source.FileSourceDto; import org.sonar.server.computation.analysis.AnalysisMetadataHolder; import org.sonar.server.computation.batch.BatchReportReader; import org.sonar.server.computation.component.Component; -import org.sonar.server.source.SourceService; +import org.sonar.server.computation.source.SourceHashRepository; import static com.google.common.base.Preconditions.checkNotNull; @@ -42,15 +42,15 @@ public class ScmInfoRepositoryImpl implements ScmInfoRepository { private final BatchReportReader batchReportReader; private final AnalysisMetadataHolder analysisMetadataHolder; private final DbClient dbClient; - private final SourceService sourceService; + private final SourceHashRepository sourceHashRepository; private final Map<Component, ScmInfo> scmInfoCache = new HashMap<>(); - public ScmInfoRepositoryImpl(BatchReportReader batchReportReader, AnalysisMetadataHolder analysisMetadataHolder, DbClient dbClient, SourceService sourceService) { + public ScmInfoRepositoryImpl(BatchReportReader batchReportReader, AnalysisMetadataHolder analysisMetadataHolder, DbClient dbClient, SourceHashRepository sourceHashRepository) { this.batchReportReader = batchReportReader; this.analysisMetadataHolder = analysisMetadataHolder; this.dbClient = dbClient; - this.sourceService = sourceService; + this.sourceHashRepository = sourceHashRepository; } @Override @@ -80,26 +80,26 @@ public class ScmInfoRepositoryImpl implements ScmInfoRepository { return getScmInfoFromReport(component, changesets); } - private Optional<ScmInfo> getScmInfoFromDb(Component component) { + private Optional<ScmInfo> getScmInfoFromDb(Component file) { if (analysisMetadataHolder.isFirstAnalysis()) { return Optional.absent(); } - LOGGER.trace("Reading SCM info from db for file '{}'", component); + LOGGER.trace("Reading SCM info from db for file '{}'", file.getKey()); DbSession dbSession = dbClient.openSession(false); try { - Optional<Iterable<DbFileSources.Line>> linesOpt = sourceService.getLines(dbSession, component.getUuid(), 1, Integer.MAX_VALUE); - if (linesOpt.isPresent()) { - return DbScmInfo.create(component, linesOpt.get()); + FileSourceDto dto = dbClient.fileSourceDao().selectSourceByFileUuid(dbSession, file.getUuid()); + if (dto == null || !sourceHashRepository.getRawSourceHash(file).equals(dto.getSrcHash())) { + return Optional.absent(); } - return Optional.absent(); + return DbScmInfo.create(file, dto.getSourceData().getLinesList()); } finally { dbClient.closeSession(dbSession); } } - private static Optional<ScmInfo> getScmInfoFromReport(Component component, BatchReport.Changesets changesets) { - LOGGER.trace("Reading SCM info from report for file '{}'", component); + private static Optional<ScmInfo> getScmInfoFromReport(Component file, BatchReport.Changesets changesets) { + LOGGER.trace("Reading SCM info from report for file '{}'", file.getKey()); return Optional.<ScmInfo>of(new ReportScmInfo(changesets)); } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/source/ComputeFileSourceData.java b/server/sonar-server/src/main/java/org/sonar/server/computation/source/ComputeFileSourceData.java index c4a6d285c5f..1b86cda3504 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/computation/source/ComputeFileSourceData.java +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/source/ComputeFileSourceData.java @@ -20,16 +20,13 @@ package org.sonar.server.computation.source; -import java.security.MessageDigest; +import com.google.common.base.Joiner; import java.util.Iterator; import java.util.List; -import org.apache.commons.codec.binary.Hex; -import org.apache.commons.codec.digest.DigestUtils; -import org.apache.commons.lang.StringUtils; +import org.sonar.core.hash.SourceHashComputer; +import org.sonar.core.hash.SourceLinesHashesComputer; import org.sonar.db.protobuf.DbFileSources; -import static java.nio.charset.StandardCharsets.UTF_8; - public class ComputeFileSourceData { private final List<LineReader> lineReaders; @@ -46,7 +43,7 @@ public class ComputeFileSourceData { } public Data compute() { - Data data = new Data(); + Data data = new Data(numberOfLines); while (linesIterator.hasNext()) { currentLine++; read(data, linesIterator.next(), hasNextLine()); @@ -60,13 +57,8 @@ public class ComputeFileSourceData { } private void read(Data data, String source, boolean hasNextLine) { - if (hasNextLine) { - data.lineHashes.append(computeLineChecksum(source)).append("\n"); - data.srcMd5Digest.update((source + "\n").getBytes(UTF_8)); - } else { - data.lineHashes.append(computeLineChecksum(source)); - data.srcMd5Digest.update(source.getBytes(UTF_8)); - } + data.linesHashesComputer.addLine(source); + data.sourceHashComputer.addLine(source, hasNextLine); DbFileSources.Line.Builder lineBuilder = data.fileSourceBuilder.addLinesBuilder() .setSource(source) @@ -76,39 +68,32 @@ public class ComputeFileSourceData { } } - private static String computeLineChecksum(String line) { - String reducedLine = StringUtils.replaceChars(line, "\t ", ""); - if (reducedLine.isEmpty()) { - return ""; - } - return DigestUtils.md5Hex(reducedLine); - } - private boolean hasNextLine() { return linesIterator.hasNext() || currentLine < numberOfLines; } public static class Data { - private final StringBuilder lineHashes; - private final MessageDigest srcMd5Digest; - private final DbFileSources.Data.Builder fileSourceBuilder; - - public Data() { - this.fileSourceBuilder = DbFileSources.Data.newBuilder(); - this.lineHashes = new StringBuilder(); - this.srcMd5Digest = DigestUtils.getMd5Digest(); + private static final Joiner LINE_RETURN_JOINER = Joiner.on('\n'); + + private final SourceLinesHashesComputer linesHashesComputer; + private final SourceHashComputer sourceHashComputer = new SourceHashComputer(); + private final DbFileSources.Data.Builder fileSourceBuilder = DbFileSources.Data.newBuilder(); + + public Data(int lineCount) { + this.linesHashesComputer = new SourceLinesHashesComputer(lineCount); } public String getSrcHash() { - return Hex.encodeHexString(srcMd5Digest.digest()); + return sourceHashComputer.getHash(); } public String getLineHashes() { - return lineHashes.toString(); + return LINE_RETURN_JOINER.join(linesHashesComputer.getLineHashes()); } public DbFileSources.Data getFileSourceData() { return fileSourceBuilder.build(); } } + } diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/source/SourceHashRepository.java b/server/sonar-server/src/main/java/org/sonar/server/computation/source/SourceHashRepository.java new file mode 100644 index 00000000000..ceae0fb1f15 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/source/SourceHashRepository.java @@ -0,0 +1,39 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 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.server.computation.source; + +import org.sonar.server.computation.component.Component; + +public interface SourceHashRepository { + + /** + * The hash of the source of the specified FILE component in the analysis report. + * <p> + * The source hash will be cached by the repository so that only the first call to this method will cost a file + * access on disk. + * </p> + * + * @throws NullPointerException if specified component is {@code null} + * @throws IllegalArgumentException if specified component if not a {@link Component.Type#FILE} + * @throws IllegalStateException if source hash for the specified component can not be computed + */ + String getRawSourceHash(Component file); + +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/source/SourceHashRepositoryImpl.java b/server/sonar-server/src/main/java/org/sonar/server/computation/source/SourceHashRepositoryImpl.java new file mode 100644 index 00000000000..fabaca98e48 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/source/SourceHashRepositoryImpl.java @@ -0,0 +1,78 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 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.server.computation.source; + +import java.util.HashMap; +import java.util.Map; +import javax.annotation.Nullable; +import org.sonar.core.hash.SourceHashComputer; +import org.sonar.core.util.CloseableIterator; +import org.sonar.server.computation.component.Component; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; +import static java.util.Objects.requireNonNull; + +public class SourceHashRepositoryImpl implements SourceHashRepository { + private static final String SOURCE_OR_HASH_FAILURE_ERROR_MSG = "Failed to read source and compute hashes for component %s"; + + private final SourceLinesRepository sourceLinesRepository; + private final Map<String, String> rawSourceHashesByKey = new HashMap<>(); + + public SourceHashRepositoryImpl(SourceLinesRepository sourceLinesRepository) { + this.sourceLinesRepository = sourceLinesRepository; + } + + @Override + public String getRawSourceHash(Component file) { + checkComponentArgument(file); + if (rawSourceHashesByKey.containsKey(file.getKey())) { + return checkSourceHash(file.getKey(), rawSourceHashesByKey.get(file.getKey())); + } else { + String newSourceHash = computeRawSourceHash(file); + rawSourceHashesByKey.put(file.getKey(), newSourceHash); + return checkSourceHash(file.getKey(), newSourceHash); + } + } + + private static void checkComponentArgument(Component file) { + requireNonNull(file, "Specified component can not be null"); + checkArgument(file.getType() == Component.Type.FILE, "File source information can only be retrieved from FILE components (got %s)", file.getType()); + } + + private String computeRawSourceHash(Component file) { + SourceHashComputer sourceHashComputer = new SourceHashComputer(); + CloseableIterator<String> linesIterator = sourceLinesRepository.readLines(file); + try { + while (linesIterator.hasNext()) { + sourceHashComputer.addLine(linesIterator.next(), linesIterator.hasNext()); + } + return sourceHashComputer.getHash(); + } finally { + linesIterator.close(); + } + } + + private static String checkSourceHash(String fileKey, @Nullable String newSourceHash) { + checkState(newSourceHash != null, SOURCE_OR_HASH_FAILURE_ERROR_MSG, fileKey); + return newSourceHash; + } + +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/source/SourceLinesRepository.java b/server/sonar-server/src/main/java/org/sonar/server/computation/source/SourceLinesRepository.java index 9cdfc910489..8328546eddc 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/computation/source/SourceLinesRepository.java +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/source/SourceLinesRepository.java @@ -26,10 +26,16 @@ import org.sonar.server.computation.component.Component; public interface SourceLinesRepository { /** - * Return lines from a given component from the report. + * Creates a iterator over the source lines of a given component from the report. + * <p> + * The returned {@link CloseableIterator} will wrap the {@link CloseableIterator} returned by + * {@link org.sonar.server.computation.batch.BatchReportReader#readFileSource(int)} but enforces that the number + * of lines specified by {@link org.sonar.batch.protocol.output.BatchReport.Component#getLines()} is respected, adding + * an extra empty last line if required. + * </p> * * @throws NullPointerException if argument is {@code null} - * @throws IllegalArgumentException if component is not a {@link org.sonar.server.computation.component.Component.Type#FILE} + * @throws IllegalArgumentException if component is not a {@link Component.Type#FILE} * @throws IllegalStateException if the file has no source code in the report */ CloseableIterator<String> readLines(Component component); diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/source/SourceLinesRepositoryImpl.java b/server/sonar-server/src/main/java/org/sonar/server/computation/source/SourceLinesRepositoryImpl.java index 2d6e2d30a8c..a3e8ac94191 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/computation/source/SourceLinesRepositoryImpl.java +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/source/SourceLinesRepositoryImpl.java @@ -21,12 +21,13 @@ package org.sonar.server.computation.source; import com.google.common.base.Optional; -import com.google.common.base.Preconditions; import org.sonar.core.util.CloseableIterator; import org.sonar.server.computation.batch.BatchReportReader; import org.sonar.server.computation.component.Component; +import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkState; +import static java.util.Objects.requireNonNull; import static org.sonar.server.computation.component.Component.Type.FILE; public class SourceLinesRepositoryImpl implements SourceLinesRepository { @@ -39,13 +40,58 @@ public class SourceLinesRepositoryImpl implements SourceLinesRepository { @Override public CloseableIterator<String> readLines(Component component) { - Preconditions.checkNotNull(component, "Component should not be bull"); - if (!component.getType().equals(FILE)) { - throw new IllegalArgumentException(String.format("Component '%s' is not a file", component)); - } + requireNonNull(component, "Component should not be bull"); + checkArgument(component.getType() == FILE, "Component '%s' is not a file", component); Optional<CloseableIterator<String>> linesIteratorOptional = reportReader.readFileSource(component.getReportAttributes().getRef()); + checkState(linesIteratorOptional.isPresent(), String.format("File '%s' has no source code", component)); - return linesIteratorOptional.get(); + int numberOfLines = reportReader.readComponent(component.getReportAttributes().getRef()).getLines(); + CloseableIterator<String> lineIterator = linesIteratorOptional.get(); + + return new ComponentLinesCloseableIterator(lineIterator, numberOfLines); + } + + private static class ComponentLinesCloseableIterator extends CloseableIterator<String> { + private static final String EXTRA_END_LINE = ""; + private final CloseableIterator<String> delegate; + private final int numberOfLines; + private int currentLine = 0; + private boolean addedExtraLine = false; + + public ComponentLinesCloseableIterator(CloseableIterator<String> lineIterator, int numberOfLines) { + this.delegate = lineIterator; + this.numberOfLines = numberOfLines; + } + + @Override + public boolean hasNext() { + return delegate.hasNext() || (currentLine < numberOfLines && !addedExtraLine); + } + + @Override + public String next() { + if (!hasNext()) { + // will throw NoSuchElementException + return delegate.next(); + } + + currentLine++; + if (delegate.hasNext()) { + return delegate.next(); + } + addedExtraLine = true; + return EXTRA_END_LINE; + } + + @Override + protected String doNext() { + throw new UnsupportedOperationException("No implemented because hasNext and next are override"); + } + + @Override + protected void doClose() throws Exception { + delegate.close(); + } } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/scm/ScmInfoRepositoryImplTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/scm/ScmInfoRepositoryImplTest.java index e6f10b19dd1..8a784bf9043 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/computation/scm/ScmInfoRepositoryImplTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/computation/scm/ScmInfoRepositoryImplTest.java @@ -20,10 +20,14 @@ package org.sonar.server.computation.scm; +import com.google.common.collect.ImmutableList; import com.tngtech.java.junit.dataprovider.DataProvider; import com.tngtech.java.junit.dataprovider.DataProviderRunner; import com.tngtech.java.junit.dataprovider.UseDataProvider; import java.util.EnumSet; +import java.util.Iterator; +import java.util.List; +import javax.annotation.Nullable; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; @@ -42,7 +46,10 @@ import org.sonar.server.computation.batch.BatchReportReaderRule; import org.sonar.server.computation.component.Component; import org.sonar.server.computation.component.ReportComponent; import org.sonar.server.computation.component.ViewsComponent; -import org.sonar.server.source.SourceService; +import org.sonar.core.hash.SourceHashComputer; +import org.sonar.server.computation.source.SourceHashRepository; +import org.sonar.server.computation.source.SourceHashRepositoryImpl; +import org.sonar.server.computation.source.SourceLinesRepositoryImpl; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.guava.api.Assertions.assertThat; @@ -54,9 +61,10 @@ import static org.sonar.server.computation.component.ReportComponent.builder; @RunWith(DataProviderRunner.class) public class ScmInfoRepositoryImplTest { - static final int FILE_REF = 1; - - static final Component FILE = builder(Component.Type.FILE, FILE_REF).setKey("FILE_KEY").setUuid("FILE_UUID").build(); + private static final int FILE_REF = 1; + private static final Component FILE = builder(Component.Type.FILE, FILE_REF).setKey("FILE_KEY").setUuid("FILE_UUID").build(); + private static final long DATE_1 = 123456789L; + private static final long DATE_2 = 1234567810L; @Rule public ExpectedException thrown = ExpectedException.none(); @@ -71,42 +79,54 @@ public class ScmInfoRepositoryImplTest { DbClient dbClient = dbTester.getDbClient(); - ScmInfoRepositoryImpl underTest = new ScmInfoRepositoryImpl(reportReader, analysisMetadataHolder, dbClient, new SourceService(dbClient, null)); + ScmInfoRepositoryImpl underTest = new ScmInfoRepositoryImpl(reportReader, analysisMetadataHolder, dbClient, + new SourceHashRepositoryImpl(new SourceLinesRepositoryImpl(reportReader))); @Test public void read_from_report() throws Exception { analysisMetadataHolder.setIsFirstAnalysis(false); - addChangesetInReport("john", 123456789L, "rev-1"); + addChangesetInReport("john", DATE_1, "rev-1"); ScmInfo scmInfo = underTest.getScmInfo(FILE).get(); assertThat(scmInfo.getAllChangesets()).hasSize(1); - assertThat(logTester.logs(TRACE)).containsOnly("Reading SCM info from report for file 'ReportComponent{ref=1, key='FILE_KEY', type=FILE}'"); + assertThat(logTester.logs(TRACE)).containsOnly("Reading SCM info from report for file 'FILE_KEY'"); } @Test - public void read_from_db() throws Exception { + public void getScmInfo_returns_ScmInfo_from_DB_if_hashes_are_the_same() throws Exception { analysisMetadataHolder.setIsFirstAnalysis(false); - addChangesetInDb("henry", 123456789L, "rev-1"); + addFileSourceInDb("henry", DATE_1, "rev-1", computeSourceHash(1)); + addFileSourceInReport(1); ScmInfo scmInfo = underTest.getScmInfo(FILE).get(); assertThat(scmInfo.getAllChangesets()).hasSize(1); - assertThat(logTester.logs(TRACE)).containsOnly("Reading SCM info from db for file 'ReportComponent{ref=1, key='FILE_KEY', type=FILE}'"); + assertThat(logTester.logs(TRACE)).containsOnly("Reading SCM info from db for file 'FILE_KEY'"); } @Test - public void read_from_report_even_if_data_in_db_exists() throws Exception { + public void getScmInfo_returns_absent_if_hash_from_db_does_not_match() throws Exception { analysisMetadataHolder.setIsFirstAnalysis(false); - addChangesetInDb("henry", 123456789L, "rev-1"); + addFileSourceInDb("henry", DATE_1, "rev-1", computeSourceHash(1) + "_different"); + addFileSourceInReport(1); - addChangesetInReport("john", 1234567810L, "rev-2"); + assertThat(underTest.getScmInfo(FILE)).isAbsent(); + + assertThat(logTester.logs(TRACE)).containsOnly("Reading SCM info from db for file 'FILE_KEY'"); + } + + @Test + public void read_from_report_even_if_data_in_db_exists() throws Exception { + analysisMetadataHolder.setIsFirstAnalysis(false); + addFileSourceInDb("henry", DATE_1, "rev-1", computeSourceHash(1)); + addChangesetInReport("john", DATE_2, "rev-2"); ScmInfo scmInfo = underTest.getScmInfo(FILE).get(); Changeset changeset = scmInfo.getChangesetForLine(1); assertThat(changeset.getAuthor()).isEqualTo("john"); - assertThat(changeset.getDate()).isEqualTo(1234567810L); + assertThat(changeset.getDate()).isEqualTo(DATE_2); assertThat(changeset.getRevision()).isEqualTo("rev-2"); } @@ -119,13 +139,8 @@ public class ScmInfoRepositoryImplTest { @Test public void return_nothing_when_nothing_in_report_and_db_has_no_scm() throws Exception { analysisMetadataHolder.setIsFirstAnalysis(false); - DbFileSources.Data.Builder fileDataBuilder = DbFileSources.Data.newBuilder(); - fileDataBuilder.addLinesBuilder() - .setLine(1); - dbTester.getDbClient().fileSourceDao().insert(new FileSourceDto() - .setFileUuid(FILE.getUuid()) - .setProjectUuid("PROJECT_UUID") - .setSourceData(fileDataBuilder.build())); + addFileSourceInDb(null, null, null, "don't care"); + addFileSourceInReport(1); assertThat(underTest.getScmInfo(FILE)).isAbsent(); } @@ -141,7 +156,7 @@ public class ScmInfoRepositoryImplTest { } @DataProvider - public static Object[][] allTypeComponentButFile() { + public static Object[][] allTypeComponentButFile() { Object[][] res = new Object[Component.Type.values().length - 1][1]; int i = 0; for (Component.Type type : EnumSet.complementOf(EnumSet.of(Component.Type.FILE))) { @@ -161,18 +176,18 @@ public class ScmInfoRepositoryImplTest { BatchReportReader batchReportReader = mock(BatchReportReader.class); AnalysisMetadataHolder analysisMetadataHolder = mock(AnalysisMetadataHolder.class); DbClient dbClient = mock(DbClient.class); - SourceService sourceService = mock(SourceService.class); - ScmInfoRepositoryImpl underTest = new ScmInfoRepositoryImpl(batchReportReader, analysisMetadataHolder, dbClient, sourceService); + SourceHashRepository sourceHashRepository = mock(SourceHashRepository.class); + ScmInfoRepositoryImpl underTest = new ScmInfoRepositoryImpl(batchReportReader, analysisMetadataHolder, dbClient, sourceHashRepository); assertThat(underTest.getScmInfo(component)).isAbsent(); - verifyNoMoreInteractions(batchReportReader, analysisMetadataHolder, dbClient, sourceService); + verifyNoMoreInteractions(batchReportReader, analysisMetadataHolder, dbClient, sourceHashRepository); } @Test public void load_scm_info_from_cache_when_already_read() throws Exception { analysisMetadataHolder.setIsFirstAnalysis(false); - addChangesetInReport("john", 123456789L, "rev-1"); + addChangesetInReport("john", DATE_1, "rev-1"); ScmInfo scmInfo = underTest.getScmInfo(FILE).get(); assertThat(scmInfo.getAllChangesets()).hasSize(1); @@ -186,23 +201,31 @@ public class ScmInfoRepositoryImplTest { @Test public void not_read_in_db_on_first_analysis() throws Exception { analysisMetadataHolder.setIsFirstAnalysis(true); - addChangesetInDb("henry", 123456789L, "rev-1"); + addFileSourceInDb("henry", DATE_1, "rev-1", "don't care"); + addFileSourceInReport(1); assertThat(underTest.getScmInfo(FILE)).isAbsent(); assertThat(logTester.logs(TRACE)).isEmpty(); } - private void addChangesetInDb(String author, Long date, String revision) { + private void addFileSourceInDb(@Nullable String author, @Nullable Long date, @Nullable String revision, String srcHash) { DbFileSources.Data.Builder fileDataBuilder = DbFileSources.Data.newBuilder(); - fileDataBuilder.addLinesBuilder() - .setLine(1) - .setScmAuthor(author) - .setScmDate(date) - .setScmRevision(revision); + DbFileSources.Line.Builder builder = fileDataBuilder.addLinesBuilder() + .setLine(1); + if (author != null) { + builder.setScmAuthor(author); + } + if (date != null) { + builder.setScmDate(date); + } + if (revision != null) { + builder.setScmRevision(revision); + } dbTester.getDbClient().fileSourceDao().insert(new FileSourceDto() .setFileUuid(FILE.getUuid()) .setProjectUuid("PROJECT_UUID") - .setSourceData(fileDataBuilder.build())); + .setSourceData(fileDataBuilder.build()) + .setSrcHash(srcHash)); } private void addChangesetInReport(String author, Long date, String revision) { @@ -216,4 +239,29 @@ public class ScmInfoRepositoryImplTest { .addChangesetIndexByLine(0) .build()); } + + private void addFileSourceInReport(int lineCount) { + reportReader.putFileSourceLines(FILE_REF, generateLines(lineCount)); + reportReader.putComponent(BatchReport.Component.newBuilder() + .setRef(FILE_REF) + .setLines(lineCount) + .build()); + } + + private static List<String> generateLines(int lineCount) { + ImmutableList.Builder<String> builder = ImmutableList.builder(); + for (int i = 0; i < lineCount; i++) { + builder.add("line " + i); + } + return builder.build(); + } + + private static String computeSourceHash(int lineCount) { + SourceHashComputer sourceHashComputer = new SourceHashComputer(); + Iterator<String> lines = generateLines(lineCount).iterator(); + while (lines.hasNext()) { + sourceHashComputer.addLine(lines.next(), lines.hasNext()); + } + return sourceHashComputer.getHash(); + } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/source/SourceHashRepositoryImplTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/source/SourceHashRepositoryImplTest.java new file mode 100644 index 00000000000..01b19fd8915 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/computation/source/SourceHashRepositoryImplTest.java @@ -0,0 +1,149 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 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.server.computation.source; + +import com.google.common.base.Function; +import com.google.common.base.Predicate; +import com.google.common.collect.FluentIterable; +import com.tngtech.java.junit.dataprovider.DataProvider; +import com.tngtech.java.junit.dataprovider.DataProviderRunner; +import com.tngtech.java.junit.dataprovider.UseDataProvider; +import java.util.Arrays; +import javax.annotation.Nullable; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.sonar.core.hash.SourceHashComputer; +import org.sonar.core.util.CloseableIterator; +import org.sonar.server.computation.component.Component; +import org.sonar.server.computation.component.ReportComponent; +import org.sonar.server.computation.component.ViewsComponent; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(DataProviderRunner.class) +public class SourceHashRepositoryImplTest { + private static final int FILE_REF = 112; + private static final String FILE_KEY = "file key"; + private static final Component FILE_COMPONENT = ReportComponent.builder(Component.Type.FILE, FILE_REF).setKey(FILE_KEY).build(); + private static final String[] SOME_LINES = {"line 1", "line after line 1", "line 4 minus 1", "line 100 by 10"}; + + @Rule + public SourceLinesRepositoryRule sourceLinesRepository = new SourceLinesRepositoryRule(); + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + private SourceLinesRepository mockedSourceLinesRepository = mock(SourceLinesRepository.class); + + private SourceHashRepositoryImpl underTest = new SourceHashRepositoryImpl(sourceLinesRepository); + private SourceHashRepositoryImpl mockedUnderTest = new SourceHashRepositoryImpl(mockedSourceLinesRepository); + + @Test + public void getRawSourceHash_throws_NPE_if_Component_argument_is_null() { + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("Specified component can not be null"); + + underTest.getRawSourceHash(null); + } + + @Test + @UseDataProvider("componentsOfAllTypesButFile") + public void getRawSourceHash_throws_IAE_if_Component_argument_is_not_FILE(Component component) { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("File source information can only be retrieved from FILE components (got " + component.getType() + ")"); + + underTest.getRawSourceHash(component); + } + + @DataProvider + public static Object[][] componentsOfAllTypesButFile() { + return FluentIterable.from(Arrays.asList(Component.Type.values())) + .filter(new Predicate<Component.Type>() { + @Override + public boolean apply(@Nullable Component.Type input) { + return input != Component.Type.FILE; + } + }) + .transform(new Function<Component.Type, Component>() { + @Nullable + @Override + public Component apply(Component.Type input) { + if (input.isReportType()) { + return ReportComponent.builder(input, input.hashCode()) + .setKey(input.name() + "_key") + .build(); + } else if (input.isViewsType()) { + return ViewsComponent.builder(input, input.name() + "_key") + .build(); + } else { + throw new IllegalArgumentException("Unsupported type " + input); + } + } + }).transform(new Function<Component, Component[]>() { + @Nullable + @Override + public Component[] apply(@Nullable Component input) { + return new Component[] { input }; + } + }).toArray(Component[].class); + + } + + @Test + public void getRawSourceHash_returns_hash_of_lines_from_SourceLinesRepository() { + sourceLinesRepository.addLines(FILE_REF, SOME_LINES); + + String rawSourceHash = underTest.getRawSourceHash(FILE_COMPONENT); + + SourceHashComputer sourceHashComputer = new SourceHashComputer(); + for (int i = 0; i < SOME_LINES.length; i++) { + sourceHashComputer.addLine(SOME_LINES[i], i < (SOME_LINES.length - 1)); + } + + assertThat(rawSourceHash).isEqualTo(sourceHashComputer.getHash()); + } + + @Test + public void getRawSourceHash_reads_lines_from_SourceLinesRepository_only_the_first_time() { + when(mockedSourceLinesRepository.readLines(FILE_COMPONENT)).thenReturn(CloseableIterator.from(Arrays.asList(SOME_LINES).iterator())); + + String rawSourceHash = mockedUnderTest.getRawSourceHash(FILE_COMPONENT); + String rawSourceHash1 = mockedUnderTest.getRawSourceHash(FILE_COMPONENT); + + assertThat(rawSourceHash).isSameAs(rawSourceHash1); + verify(mockedSourceLinesRepository, times(1)).readLines(FILE_COMPONENT); + } + + @Test + public void getRawSourceHash_let_exception_go_through() { + IllegalArgumentException thrown = new IllegalArgumentException("this IAE will cause the hash computation to fail"); + when(mockedSourceLinesRepository.readLines(FILE_COMPONENT)).thenThrow(thrown); + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage(thrown.getMessage()); + + mockedUnderTest.getRawSourceHash(FILE_COMPONENT); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/source/SourceLinesRepositoryImplTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/source/SourceLinesRepositoryImplTest.java index 35c75a44f96..2c929649b1c 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/computation/source/SourceLinesRepositoryImplTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/computation/source/SourceLinesRepositoryImplTest.java @@ -23,6 +23,7 @@ package org.sonar.server.computation.source; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import org.sonar.batch.protocol.output.BatchReport; import org.sonar.server.computation.batch.BatchReportReaderRule; import org.sonar.server.computation.component.Component; @@ -50,14 +51,32 @@ public class SourceLinesRepositoryImplTest { @Test public void read_lines_from_report() throws Exception { + reportReader.putComponent(createFileBatchComponent(2)); reportReader.putFileSourceLines(FILE_REF, "line1", "line2"); assertThat(underTest.readLines(FILE)).containsOnly("line1", "line2"); } @Test + public void read_lines_add_at_most_one_extra_empty_line_when_sourceLine_has_less_elements_then_lineCount() throws Exception { + reportReader.putComponent(createFileBatchComponent(10)); + reportReader.putFileSourceLines(FILE_REF, "line1", "line2"); + + assertThat(underTest.readLines(FILE)).containsOnly("line1", "line2", ""); + } + + @Test + public void read_lines_reads_all_lines_from_sourceLines_when_it_has_more_elements_then_lineCount() throws Exception { + reportReader.putComponent(createFileBatchComponent(2)); + reportReader.putFileSourceLines(FILE_REF, "line1", "line2", "line3"); + + assertThat(underTest.readLines(FILE)).containsOnly("line1", "line2", "line3"); + } + + @Test public void not_fail_to_read_lines_on_empty_file_from_report() throws Exception { // File exist but there's no line + reportReader.putComponent(createFileBatchComponent(0)); reportReader.putFileSourceLines(FILE_REF); // Should not try to read source file from the db @@ -88,4 +107,8 @@ public class SourceLinesRepositoryImplTest { underTest.readLines(builder(Component.Type.PROJECT, 123).setKey("NotFile").build()); } + private static BatchReport.Component createFileBatchComponent(int lineCount) { + return BatchReport.Component.newBuilder().setRef(FILE_REF).setLines(lineCount).build(); + } + } diff --git a/sonar-core/src/main/java/org/sonar/core/hash/SourceHashComputer.java b/sonar-core/src/main/java/org/sonar/core/hash/SourceHashComputer.java new file mode 100644 index 00000000000..03a608d477e --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/core/hash/SourceHashComputer.java @@ -0,0 +1,43 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 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.core.hash; + +import java.security.MessageDigest; +import org.apache.commons.codec.binary.Hex; +import org.apache.commons.codec.digest.DigestUtils; + +import static java.nio.charset.StandardCharsets.UTF_8; + +/** + * Computes the hash of the source lines of a file by simply added lines of that file one by one in order with + * {@link #addLine(String, boolean)}. + */ +public class SourceHashComputer { + private final MessageDigest md5Digest = DigestUtils.getMd5Digest(); + + public void addLine(String line, boolean hasNextLine) { + String lineToHash = hasNextLine ? line + '\n' : line; + this.md5Digest.update(lineToHash.getBytes(UTF_8)); + } + + public String getHash() { + return Hex.encodeHexString(md5Digest.digest()); + } +} diff --git a/sonar-core/src/main/java/org/sonar/core/hash/SourceLinesHashesComputer.java b/sonar-core/src/main/java/org/sonar/core/hash/SourceLinesHashesComputer.java new file mode 100644 index 00000000000..fb0696d9e4f --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/core/hash/SourceLinesHashesComputer.java @@ -0,0 +1,65 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 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.core.hash; + +import com.google.common.collect.ImmutableList; +import java.security.MessageDigest; +import java.util.ArrayList; +import java.util.List; +import org.apache.commons.codec.binary.Hex; +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.lang.StringUtils; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Objects.requireNonNull; + +/** + * Computes the hash of each line of a given file by simply added lines of that file one by one in order with + * {@link #addLine(String)}. + */ +public class SourceLinesHashesComputer { + private final MessageDigest md5Digest = DigestUtils.getMd5Digest(); + private final List<String> lineHashes; + + public SourceLinesHashesComputer() { + this.lineHashes = new ArrayList<>(); + } + + public SourceLinesHashesComputer(int expectedLineCount) { + this.lineHashes = new ArrayList<>(expectedLineCount); + } + + public void addLine(String line) { + requireNonNull(line, "line can not be null"); + lineHashes.add(computeHash(line)); + } + + public List<String> getLineHashes() { + return ImmutableList.copyOf(lineHashes); + } + + private String computeHash(String line) { + String reducedLine = StringUtils.replaceChars(line, "\t ", ""); + if (reducedLine.isEmpty()) { + return ""; + } + return Hex.encodeHexString(md5Digest.digest(reducedLine.getBytes(UTF_8))); + } +} diff --git a/sonar-core/src/main/java/org/sonar/core/hash/package-info.java b/sonar-core/src/main/java/org/sonar/core/hash/package-info.java new file mode 100644 index 00000000000..4ac93cc3f0d --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/core/hash/package-info.java @@ -0,0 +1,25 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 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.core.hash; + +import javax.annotation.ParametersAreNonnullByDefault; + diff --git a/sonar-core/src/main/java/org/sonar/core/issue/tracking/LineHashSequence.java b/sonar-core/src/main/java/org/sonar/core/issue/tracking/LineHashSequence.java index b6be6b731af..2601ccd05b5 100644 --- a/sonar-core/src/main/java/org/sonar/core/issue/tracking/LineHashSequence.java +++ b/sonar-core/src/main/java/org/sonar/core/issue/tracking/LineHashSequence.java @@ -20,13 +20,11 @@ package org.sonar.core.issue.tracking; import com.google.common.base.Strings; -import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.annotation.Nullable; -import org.apache.commons.codec.digest.DigestUtils; -import org.apache.commons.lang.StringUtils; +import org.sonar.core.hash.SourceLinesHashesComputer; /** * Sequence of hash of lines for a given file @@ -100,20 +98,12 @@ public class LineHashSequence { return result; } - public static LineHashSequence createForLines(Iterable<String> lines) { - List<String> hashes = new ArrayList<>(); + public static LineHashSequence createForLines(List<String> lines) { + SourceLinesHashesComputer hashesComputer = new SourceLinesHashesComputer(lines.size()); for (String line : lines) { - hashes.add(hash(line)); + hashesComputer.addLine(line); } - return new LineHashSequence(hashes); + return new LineHashSequence(hashesComputer.getLineHashes()); } - // FIXME duplicates ComputeFileSourceData - private static String hash(String line) { - String reducedLine = StringUtils.replaceChars(line, "\t ", ""); - if (reducedLine.isEmpty()) { - return ""; - } - return DigestUtils.md5Hex(reducedLine); - } } diff --git a/sonar-core/src/test/java/org/sonar/core/hash/SourceHashComputerTest.java b/sonar-core/src/test/java/org/sonar/core/hash/SourceHashComputerTest.java new file mode 100644 index 00000000000..a096022f1d7 --- /dev/null +++ b/sonar-core/src/test/java/org/sonar/core/hash/SourceHashComputerTest.java @@ -0,0 +1,56 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 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.core.hash; + +import java.nio.charset.StandardCharsets; +import org.apache.commons.codec.digest.DigestUtils; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class SourceHashComputerTest { + + private static final String SOME_LINE = "some line"; + + @Test + public void hash_of_non_last_line_is_not_the_same_as_hash_of_last_line() { + assertThat(hashASingleLine(SOME_LINE, true)).isNotEqualTo(hashASingleLine(SOME_LINE, false)); + } + + @Test + public void hash_of_non_last_line_is_includes_an_added_line_return() { + assertThat(hashASingleLine(SOME_LINE, true)).isEqualTo(hashASingleLine(SOME_LINE + '\n', false)); + } + + @Test + public void hash_is_md5_digest_of_UTF8_character_array_in_hexa_encoding() { + String someLineWithAccents = "yopa l\u00e9l\u00e0"; + + assertThat(hashASingleLine(someLineWithAccents, false)) + .isEqualTo(DigestUtils.md5Hex(someLineWithAccents.getBytes(StandardCharsets.UTF_8))); + + } + + private static String hashASingleLine(String line, boolean hasNextLine) { + SourceHashComputer sourceHashComputer = new SourceHashComputer(); + sourceHashComputer.addLine(line, hasNextLine); + return sourceHashComputer.getHash(); + } +} diff --git a/sonar-core/src/test/java/org/sonar/core/hash/SourceLinesHashesComputerTest.java b/sonar-core/src/test/java/org/sonar/core/hash/SourceLinesHashesComputerTest.java new file mode 100644 index 00000000000..7591b3c2579 --- /dev/null +++ b/sonar-core/src/test/java/org/sonar/core/hash/SourceLinesHashesComputerTest.java @@ -0,0 +1,87 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 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.core.hash; + +import java.nio.charset.StandardCharsets; +import javax.annotation.Nullable; +import org.apache.commons.codec.digest.DigestUtils; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static org.assertj.core.api.Assertions.assertThat; + +public class SourceLinesHashesComputerTest { + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Test + public void addLine_throws_NPE_is_line_null() { + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("line can not be null"); + + new SourceLinesHashesComputer(1).addLine(null); + } + + @Test + public void hash_of_empty_string_is_empty_string() { + assertThat(hashSingleLine("")).isEqualTo(""); + } + + @Test + public void tab_and_spaces_are_ignored_from_hash() { + assertThat(hashSingleLine(" ")).isEqualTo(""); + assertThat(hashSingleLine("\t")).isEqualTo(""); + assertThat(hashSingleLine("\t \t \t\t ")).isEqualTo(""); + + String abHash = hashSingleLine("ab"); + assertThat(hashSingleLine("a b")).isEqualTo(abHash); + assertThat(hashSingleLine("a\tb")).isEqualTo(abHash); + assertThat(hashSingleLine("\t a\t \tb\t ")).isEqualTo(abHash); + } + + @Test + public void hash_of_line_is_md5_of_UTF_char_array_as_an_hex_string() { + String lineWithAccentAndSpace = "Yolo lélà"; + assertThat(hashSingleLine(lineWithAccentAndSpace)).isEqualTo( + DigestUtils.md5Hex("Yololélà".getBytes(StandardCharsets.UTF_8))); + } + + @Test + public void getLineHashes_returns_line_hash_in_order_of_addLine_calls() { + String line1 = "line 1"; + String line2 = "line 1 + 1"; + String line3 = "line 10 - 7"; + + SourceLinesHashesComputer underTest = new SourceLinesHashesComputer(); + underTest.addLine(line1); + underTest.addLine(line2); + underTest.addLine(line3); + + assertThat(underTest.getLineHashes()).containsExactly( + hashSingleLine(line1), hashSingleLine(line2), hashSingleLine(line3)); + } + + private static String hashSingleLine(@Nullable String line) { + SourceLinesHashesComputer sourceLinesHashesComputer = new SourceLinesHashesComputer(1); + sourceLinesHashesComputer.addLine(line); + return sourceLinesHashesComputer.getLineHashes().iterator().next(); + } +} |