From 7cf051c0f7d8ec017381b05c85044e9321c6c4f7 Mon Sep 17 00:00:00 2001 From: Evgeny Mandrikov Date: Wed, 31 Aug 2011 19:27:13 +0400 Subject: SONAR-1091 Add CPD over different projects * Add table clone_blocks * Add DbCloneIndex, which can be activated in sonar-cpd-plugin using property "sonar.cpd.cross_project=true" --- .../main/resources/org/sonar/test/persistence/sonar-test.ddl | 11 +++++++++++ 1 file changed, 11 insertions(+) (limited to 'sonar-testing-harness/src') diff --git a/sonar-testing-harness/src/main/resources/org/sonar/test/persistence/sonar-test.ddl b/sonar-testing-harness/src/main/resources/org/sonar/test/persistence/sonar-test.ddl index 864fff1f2a2..1c8c3eb23fe 100644 --- a/sonar-testing-harness/src/main/resources/org/sonar/test/persistence/sonar-test.ddl +++ b/sonar-testing-harness/src/main/resources/org/sonar/test/persistence/sonar-test.ddl @@ -487,3 +487,14 @@ CREATE TABLE REVIEW_COMMENTS ( REVIEW_TEXT CLOB(2147483647), primary key (id) ); + +CREATE TABLE CLONE_BLOCKS ( + SNAPSHOT_ID INTEGER, + HASH VARCHAR(50), + RESOURCE_KEY VARCHAR(400), + INDEX_IN_FILE INTEGER NOT NULL, + START_LINE INTEGER NOT NULL, + END_LINE INTEGER NOT NULL +); +CREATE INDEX CLONE_BLOCKS_HASH ON CLONE_BLOCKS (HASH); +CREATE INDEX CLONE_BLOCKS_RESOURCE ON CLONE_BLOCKS (SNAPSHOT_ID, RESOURCE_KEY); -- cgit v1.2.3 From 5ff8c8c74e58a9df51be059908a064a43a67c50d Mon Sep 17 00:00:00 2001 From: Evgeny Mandrikov Date: Thu, 1 Sep 2011 19:06:46 +0400 Subject: SONAR-1091 CPD over different projects * Improve table clone_blocks and so DbCloneIndex. * Add purge of old clone blocks. --- .../java/org/sonar/plugins/cpd/SonarEngine.java | 30 +++--- .../plugins/cpd/index/CombinedCloneIndex.java | 59 ----------- .../org/sonar/plugins/cpd/index/DbCloneIndex.java | 114 ++++++++++++++------- .../sonar/plugins/cpd/index/SonarCloneIndex.java | 81 +++++++++++++++ .../sonar/plugins/cpd/index/DbCloneIndexTest.java | 38 ++++--- .../plugins/cpd/index/DbCloneIndexTest/fixture.xml | 25 ----- .../cpd/index/DbCloneIndexTest/shouldGetByHash.xml | 43 ++++++++ .../index/DbCloneIndexTest/shouldInsert-result.xml | 26 +---- .../cpd/index/DbCloneIndexTest/shouldInsert.xml | 7 ++ .../sonar/plugins/dbcleaner/api/PurgeUtils.java | 9 ++ .../api/PurgeUtilsTest/purgeSnapshots-result.xml | 5 +- .../api/PurgeUtilsTest/purgeSnapshots.xml | 5 +- .../main/java/org/sonar/jpa/entity/CloneBlock.java | 16 ++- .../WEB-INF/db/migrate/217_create_clone_blocks.rb | 5 +- .../org/sonar/test/persistence/sonar-test.ddl | 5 +- 15 files changed, 276 insertions(+), 192 deletions(-) delete mode 100644 plugins/sonar-cpd-plugin/src/main/java/org/sonar/plugins/cpd/index/CombinedCloneIndex.java create mode 100644 plugins/sonar-cpd-plugin/src/main/java/org/sonar/plugins/cpd/index/SonarCloneIndex.java delete mode 100644 plugins/sonar-cpd-plugin/src/test/resources/org/sonar/plugins/cpd/index/DbCloneIndexTest/fixture.xml create mode 100644 plugins/sonar-cpd-plugin/src/test/resources/org/sonar/plugins/cpd/index/DbCloneIndexTest/shouldGetByHash.xml create mode 100644 plugins/sonar-cpd-plugin/src/test/resources/org/sonar/plugins/cpd/index/DbCloneIndexTest/shouldInsert.xml (limited to 'sonar-testing-harness/src') diff --git a/plugins/sonar-cpd-plugin/src/main/java/org/sonar/plugins/cpd/SonarEngine.java b/plugins/sonar-cpd-plugin/src/main/java/org/sonar/plugins/cpd/SonarEngine.java index b3cd865cfc2..e99cd441e9f 100644 --- a/plugins/sonar-cpd-plugin/src/main/java/org/sonar/plugins/cpd/SonarEngine.java +++ b/plugins/sonar-cpd-plugin/src/main/java/org/sonar/plugins/cpd/SonarEngine.java @@ -21,6 +21,7 @@ package org.sonar.plugins.cpd; import java.io.File; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; @@ -30,7 +31,6 @@ import java.util.Set; import org.sonar.api.batch.SensorContext; import org.sonar.api.database.DatabaseSession; import org.sonar.api.database.model.ResourceModel; -import org.sonar.api.database.model.Snapshot; import org.sonar.api.measures.CoreMetrics; import org.sonar.api.measures.Measure; import org.sonar.api.resources.InputFile; @@ -45,19 +45,15 @@ import org.sonar.duplications.block.Block; import org.sonar.duplications.block.BlockChunker; import org.sonar.duplications.detector.original.OriginalCloneDetectionAlgorithm; import org.sonar.duplications.index.CloneGroup; -import org.sonar.duplications.index.CloneIndex; import org.sonar.duplications.index.ClonePart; -import org.sonar.duplications.index.PackedMemoryCloneIndex; import org.sonar.duplications.java.JavaStatementBuilder; import org.sonar.duplications.java.JavaTokenProducer; import org.sonar.duplications.statement.Statement; import org.sonar.duplications.statement.StatementChunker; import org.sonar.duplications.token.TokenChunker; import org.sonar.duplications.token.TokenQueue; -import org.sonar.plugins.cpd.index.CombinedCloneIndex; import org.sonar.plugins.cpd.index.DbCloneIndex; - -import com.google.common.collect.Lists; +import org.sonar.plugins.cpd.index.SonarCloneIndex; public class SonarEngine implements CpdEngine { @@ -94,13 +90,12 @@ public class SonarEngine implements CpdEngine { } // Create index - CloneIndex index = new PackedMemoryCloneIndex(); + final SonarCloneIndex index; if (isCrossProject(project)) { Logs.INFO.info("Enabled cross-project analysis"); - Snapshot currentSnapshot = resourcePersister.getSnapshot(project); - Snapshot lastSnapshot = resourcePersister.getLastSnapshot(currentSnapshot, false); - DbCloneIndex db = new DbCloneIndex(dbSession, currentSnapshot.getId(), lastSnapshot == null ? null : lastSnapshot.getId()); - index = new CombinedCloneIndex(index, db); + index = new SonarCloneIndex(new DbCloneIndex(dbSession, resourcePersister, project)); + } else { + index = new SonarCloneIndex(); } TokenChunker tokenChunker = JavaTokenProducer.build(); @@ -108,21 +103,22 @@ public class SonarEngine implements CpdEngine { BlockChunker blockChunker = new BlockChunker(BLOCK_SIZE); for (InputFile inputFile : inputFiles) { + Resource resource = getResource(inputFile); + String resourceKey = getFullKey(project, resource); + File file = inputFile.getFile(); TokenQueue tokenQueue = tokenChunker.chunk(file); List statements = statementChunker.chunk(tokenQueue); - Resource resource = getResource(inputFile); - List blocks = blockChunker.chunk(getFullKey(project, resource), statements); - for (Block block : blocks) { - index.insert(block); - } + List blocks = blockChunker.chunk(resourceKey, statements); + index.insert(resource, blocks); } // Detect for (InputFile inputFile : inputFiles) { Resource resource = getResource(inputFile); + String resourceKey = getFullKey(project, resource); - List fileBlocks = Lists.newArrayList(index.getByResourceId(getFullKey(project, resource))); + Collection fileBlocks = index.getByResource(resource, resourceKey); List clones = OriginalCloneDetectionAlgorithm.detect(index, fileBlocks); if (!clones.isEmpty()) { // Save diff --git a/plugins/sonar-cpd-plugin/src/main/java/org/sonar/plugins/cpd/index/CombinedCloneIndex.java b/plugins/sonar-cpd-plugin/src/main/java/org/sonar/plugins/cpd/index/CombinedCloneIndex.java deleted file mode 100644 index 0b2f4e4c929..00000000000 --- a/plugins/sonar-cpd-plugin/src/main/java/org/sonar/plugins/cpd/index/CombinedCloneIndex.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Sonar, open source software quality management tool. - * Copyright (C) 2008-2011 SonarSource - * mailto:contact AT sonarsource DOT com - * - * Sonar 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. - * - * Sonar 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 Sonar; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package org.sonar.plugins.cpd.index; - -import java.util.Collection; -import java.util.List; - -import org.sonar.duplications.block.Block; -import org.sonar.duplications.block.ByteArray; -import org.sonar.duplications.index.AbstractCloneIndex; -import org.sonar.duplications.index.CloneIndex; - -import com.google.common.collect.Lists; - -public class CombinedCloneIndex extends AbstractCloneIndex { - - private final CloneIndex mem; - private final DbCloneIndex db; - - public CombinedCloneIndex(CloneIndex mem, DbCloneIndex db) { - this.mem = mem; - this.db = db; - } - - public Collection getByResourceId(String resourceId) { - db.prepareCache(resourceId); - return mem.getByResourceId(resourceId); - } - - public Collection getBySequenceHash(ByteArray hash) { - List result = Lists.newArrayList(); - result.addAll(mem.getBySequenceHash(hash)); - result.addAll(db.getBySequenceHash(hash)); - return result; - } - - public void insert(Block block) { - mem.insert(block); - db.insert(block); - } - -} diff --git a/plugins/sonar-cpd-plugin/src/main/java/org/sonar/plugins/cpd/index/DbCloneIndex.java b/plugins/sonar-cpd-plugin/src/main/java/org/sonar/plugins/cpd/index/DbCloneIndex.java index 06f6c481cad..571dc43d3ec 100644 --- a/plugins/sonar-cpd-plugin/src/main/java/org/sonar/plugins/cpd/index/DbCloneIndex.java +++ b/plugins/sonar-cpd-plugin/src/main/java/org/sonar/plugins/cpd/index/DbCloneIndex.java @@ -24,48 +24,88 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import javax.persistence.Query; + +import org.hibernate.ejb.HibernateQuery; +import org.hibernate.transform.Transformers; import org.sonar.api.database.DatabaseSession; +import org.sonar.api.database.model.Snapshot; +import org.sonar.api.resources.Project; +import org.sonar.api.resources.Resource; +import org.sonar.batch.index.ResourcePersister; import org.sonar.duplications.block.Block; import org.sonar.duplications.block.ByteArray; -import org.sonar.duplications.index.AbstractCloneIndex; import org.sonar.jpa.entity.CloneBlock; import com.google.common.collect.Lists; import com.google.common.collect.Maps; -public class DbCloneIndex extends AbstractCloneIndex { +public class DbCloneIndex { + + private final Map> cache = Maps.newHashMap(); - private final Map> cache = Maps.newHashMap(); + private final DatabaseSession session; + private final ResourcePersister resourcePersister; + private final int currentProjectSnapshotId; + private final Integer lastSnapshotId; - private DatabaseSession session; - private int currentSnapshotId; - private Integer lastSnapshotId; + public DbCloneIndex(DatabaseSession session, ResourcePersister resourcePersister, Project currentProject) { + this.session = session; + this.resourcePersister = resourcePersister; + Snapshot currentSnapshot = resourcePersister.getSnapshotOrFail(currentProject); + Snapshot lastSnapshot = resourcePersister.getLastSnapshot(currentSnapshot, false); + this.currentProjectSnapshotId = currentSnapshot.getId(); + this.lastSnapshotId = lastSnapshot == null ? null : lastSnapshot.getId(); + } - public DbCloneIndex(DatabaseSession session, Integer currentSnapshotId, Integer lastSnapshotId) { + /** + * For tests. + */ + DbCloneIndex(DatabaseSession session, ResourcePersister resourcePersister, Integer currentProjectSnapshotId, Integer prevSnapshotId) { this.session = session; - this.currentSnapshotId = currentSnapshotId; - this.lastSnapshotId = lastSnapshotId; + this.resourcePersister = resourcePersister; + this.currentProjectSnapshotId = currentProjectSnapshotId; + this.lastSnapshotId = prevSnapshotId; } - public void prepareCache(String resourceKey) { - String sql = "SELECT block.id, hash, block.snapshot_id, resource_key, index_in_file, start_line, end_line FROM clone_blocks AS block, snapshots AS snapshot" + - " WHERE block.snapshot_id=snapshot.id AND snapshot.islast=true" + - " AND hash IN ( SELECT hash FROM clone_blocks WHERE resource_key = :resource_key AND snapshot_id = :current_snapshot_id )"; + int getSnapshotIdFor(Resource resource) { + return resourcePersister.getSnapshotOrFail(resource).getId(); + } + + public void prepareCache(Resource resource) { + int resourceSnapshotId = getSnapshotIdFor(resource); + + // Order of columns is important - see code below! + String sql = "SELECT hash, resource.kee, index_in_file, start_line, end_line" + + " FROM clone_blocks AS block, snapshots AS snapshot, projects AS resource" + + " WHERE block.snapshot_id=snapshot.id AND snapshot.islast=true AND snapshot.project_id=resource.id" + + " AND hash IN ( SELECT hash FROM clone_blocks WHERE snapshot_id = :resource_snapshot_id AND project_snapshot_id = :current_project_snapshot_id )"; if (lastSnapshotId != null) { // Filter for blocks from previous snapshot of current project - sql += " AND snapshot.id != " + lastSnapshotId; + sql += " AND block.project_snapshot_id != :last_project_snapshot_id"; } - List blocks = session.getEntityManager() - .createNativeQuery(sql, CloneBlock.class) - .setParameter("resource_key", resourceKey) - .setParameter("current_snapshot_id", currentSnapshotId) - .getResultList(); + Query query = session.getEntityManager().createNativeQuery(sql) + .setParameter("resource_snapshot_id", resourceSnapshotId) + .setParameter("current_project_snapshot_id", currentProjectSnapshotId); + if (lastSnapshotId != null) { + query.setParameter("last_project_snapshot_id", lastSnapshotId); + } + // Ugly hack for mapping results of custom SQL query into plain list (MyBatis is coming soon) + ((HibernateQuery) query).getHibernateQuery().setResultTransformer(Transformers.TO_LIST); + List> blocks = query.getResultList(); cache.clear(); - for (CloneBlock dbBlock : blocks) { - Block block = new Block(dbBlock.getResourceKey(), new ByteArray(dbBlock.getHash()), dbBlock.getIndexInFile(), dbBlock.getStartLine(), dbBlock.getEndLine()); + for (List dbBlock : blocks) { + String hash = (String) dbBlock.get(0); + String resourceKey = (String) dbBlock.get(1); + int indexInFile = (Integer) dbBlock.get(2); + int startLine = (Integer) dbBlock.get(3); + int endLine = (Integer) dbBlock.get(4); + + Block block = new Block(resourceKey, new ByteArray(hash), indexInFile, startLine, endLine); - List sameHash = cache.get(block.getBlockHash()); + // Group blocks by hash + Collection sameHash = cache.get(block.getBlockHash()); if (sameHash == null) { sameHash = Lists.newArrayList(); cache.put(block.getBlockHash(), sameHash); @@ -74,28 +114,28 @@ public class DbCloneIndex extends AbstractCloneIndex { } } - public Collection getByResourceId(String resourceId) { - throw new UnsupportedOperationException(); - } - - public Collection getBySequenceHash(ByteArray sequenceHash) { - List result = cache.get(sequenceHash); + public Collection getByHash(ByteArray hash) { + Collection result = cache.get(hash); if (result != null) { return result; } else { - // not in cache return Collections.emptyList(); } } - public void insert(Block block) { - CloneBlock dbBlock = new CloneBlock(currentSnapshotId, - block.getBlockHash().toString(), - block.getResourceId(), - block.getIndexInFile(), - block.getFirstLineNumber(), - block.getLastLineNumber()); - session.save(dbBlock); + public void insert(Resource resource, Collection blocks) { + int resourceSnapshotId = getSnapshotIdFor(resource); + for (Block block : blocks) { + CloneBlock dbBlock = new CloneBlock( + currentProjectSnapshotId, + resourceSnapshotId, + block.getBlockHash().toString(), + block.getIndexInFile(), + block.getFirstLineNumber(), + block.getLastLineNumber()); + session.save(dbBlock); + } + session.commit(); } } diff --git a/plugins/sonar-cpd-plugin/src/main/java/org/sonar/plugins/cpd/index/SonarCloneIndex.java b/plugins/sonar-cpd-plugin/src/main/java/org/sonar/plugins/cpd/index/SonarCloneIndex.java new file mode 100644 index 00000000000..f5bd39542c1 --- /dev/null +++ b/plugins/sonar-cpd-plugin/src/main/java/org/sonar/plugins/cpd/index/SonarCloneIndex.java @@ -0,0 +1,81 @@ +/* + * Sonar, open source software quality management tool. + * Copyright (C) 2008-2011 SonarSource + * mailto:contact AT sonarsource DOT com + * + * Sonar 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. + * + * Sonar 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 Sonar; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.plugins.cpd.index; + +import java.util.Collection; +import java.util.List; + +import org.sonar.api.resources.Resource; +import org.sonar.duplications.block.Block; +import org.sonar.duplications.block.ByteArray; +import org.sonar.duplications.index.AbstractCloneIndex; +import org.sonar.duplications.index.CloneIndex; +import org.sonar.duplications.index.PackedMemoryCloneIndex; + +import com.google.common.collect.Lists; + +public class SonarCloneIndex extends AbstractCloneIndex { + + private final CloneIndex mem = new PackedMemoryCloneIndex(); + private final DbCloneIndex db; + + public SonarCloneIndex() { + this(null); + } + + public SonarCloneIndex(DbCloneIndex db) { + this.db = db; + } + + public void insert(Resource resource, Collection blocks) { + for (Block block : blocks) { + mem.insert(block); + } + if (db != null) { + db.insert(resource, blocks); + } + } + + public Collection getByResource(Resource resource, String resourceKey) { + if (db != null) { + db.prepareCache(resource); + } + return mem.getByResourceId(resourceKey); + } + + public Collection getBySequenceHash(ByteArray hash) { + if (db == null) { + return mem.getBySequenceHash(hash); + } else { + List result = Lists.newArrayList(mem.getBySequenceHash(hash)); + result.addAll(db.getByHash(hash)); + return result; + } + } + + public Collection getByResourceId(String resourceId) { + throw new UnsupportedOperationException(); + } + + public void insert(Block block) { + throw new UnsupportedOperationException(); + } + +} diff --git a/plugins/sonar-cpd-plugin/src/test/java/org/sonar/plugins/cpd/index/DbCloneIndexTest.java b/plugins/sonar-cpd-plugin/src/test/java/org/sonar/plugins/cpd/index/DbCloneIndexTest.java index 86698e89d64..5823c9f6044 100644 --- a/plugins/sonar-cpd-plugin/src/test/java/org/sonar/plugins/cpd/index/DbCloneIndexTest.java +++ b/plugins/sonar-cpd-plugin/src/test/java/org/sonar/plugins/cpd/index/DbCloneIndexTest.java @@ -21,12 +21,16 @@ package org.sonar.plugins.cpd.index; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import java.util.Arrays; import java.util.Collection; import java.util.Iterator; -import org.junit.Before; import org.junit.Test; +import org.sonar.api.resources.JavaFile; +import org.sonar.api.resources.Resource; import org.sonar.duplications.block.Block; import org.sonar.duplications.block.ByteArray; import org.sonar.jpa.test.AbstractDbUnitTestCase; @@ -35,35 +39,35 @@ public class DbCloneIndexTest extends AbstractDbUnitTestCase { private DbCloneIndex index; - @Before - public void setUp() { - index = new DbCloneIndex(getSession(), 5, 4); - } - - @Test(expected = UnsupportedOperationException.class) - public void shouldNotGetByResource() { - index.getByResourceId("foo"); - } - @Test public void shouldGetByHash() { - setupData("fixture"); + Resource resource = new JavaFile("foo"); + index = spy(new DbCloneIndex(getSession(), null, 9, 7)); + doReturn(10).when(index).getSnapshotIdFor(resource); + setupData("shouldGetByHash"); - index.prepareCache("foo"); - Collection blocks = index.getBySequenceHash(new ByteArray("aa")); + index.prepareCache(resource); + Collection blocks = index.getByHash(new ByteArray("aa")); Iterator blocksIterator = blocks.iterator(); assertThat(blocks.size(), is(1)); Block block = blocksIterator.next(); - assertThat(block.getResourceId(), is("bar-last")); + assertThat("block resourceId", block.getResourceId(), is("bar-last")); + assertThat("block hash", block.getBlockHash(), is(new ByteArray("aa"))); + assertThat("block index in file", block.getIndexInFile(), is(0)); + assertThat("block start line", block.getFirstLineNumber(), is(1)); + assertThat("block end line", block.getLastLineNumber(), is(2)); } @Test public void shouldInsert() { - setupData("fixture"); + Resource resource = new JavaFile("foo"); + index = spy(new DbCloneIndex(getSession(), null, 1, null)); + doReturn(2).when(index).getSnapshotIdFor(resource); + setupData("shouldInsert"); - index.insert(new Block("baz", new ByteArray("bb"), 0, 0, 1)); + index.insert(resource, Arrays.asList(new Block("foo", new ByteArray("bb"), 0, 1, 2))); checkTables("shouldInsert", "clone_blocks"); } diff --git a/plugins/sonar-cpd-plugin/src/test/resources/org/sonar/plugins/cpd/index/DbCloneIndexTest/fixture.xml b/plugins/sonar-cpd-plugin/src/test/resources/org/sonar/plugins/cpd/index/DbCloneIndexTest/fixture.xml deleted file mode 100644 index 95599894c1b..00000000000 --- a/plugins/sonar-cpd-plugin/src/test/resources/org/sonar/plugins/cpd/index/DbCloneIndexTest/fixture.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/plugins/sonar-cpd-plugin/src/test/resources/org/sonar/plugins/cpd/index/DbCloneIndexTest/shouldGetByHash.xml b/plugins/sonar-cpd-plugin/src/test/resources/org/sonar/plugins/cpd/index/DbCloneIndexTest/shouldGetByHash.xml new file mode 100644 index 00000000000..1dab2d464cf --- /dev/null +++ b/plugins/sonar-cpd-plugin/src/test/resources/org/sonar/plugins/cpd/index/DbCloneIndexTest/shouldGetByHash.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/sonar-cpd-plugin/src/test/resources/org/sonar/plugins/cpd/index/DbCloneIndexTest/shouldInsert-result.xml b/plugins/sonar-cpd-plugin/src/test/resources/org/sonar/plugins/cpd/index/DbCloneIndexTest/shouldInsert-result.xml index ae2767dfedb..e3e709ffc45 100644 --- a/plugins/sonar-cpd-plugin/src/test/resources/org/sonar/plugins/cpd/index/DbCloneIndexTest/shouldInsert-result.xml +++ b/plugins/sonar-cpd-plugin/src/test/resources/org/sonar/plugins/cpd/index/DbCloneIndexTest/shouldInsert-result.xml @@ -1,27 +1,9 @@ - - + + + - - - - - - - - - - - - - - - - - - - - + diff --git a/plugins/sonar-cpd-plugin/src/test/resources/org/sonar/plugins/cpd/index/DbCloneIndexTest/shouldInsert.xml b/plugins/sonar-cpd-plugin/src/test/resources/org/sonar/plugins/cpd/index/DbCloneIndexTest/shouldInsert.xml new file mode 100644 index 00000000000..940281a0599 --- /dev/null +++ b/plugins/sonar-cpd-plugin/src/test/resources/org/sonar/plugins/cpd/index/DbCloneIndexTest/shouldInsert.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/plugins/sonar-dbcleaner-plugin/src/main/java/org/sonar/plugins/dbcleaner/api/PurgeUtils.java b/plugins/sonar-dbcleaner-plugin/src/main/java/org/sonar/plugins/dbcleaner/api/PurgeUtils.java index c417befec67..8a7451b9d80 100644 --- a/plugins/sonar-dbcleaner-plugin/src/main/java/org/sonar/plugins/dbcleaner/api/PurgeUtils.java +++ b/plugins/sonar-dbcleaner-plugin/src/main/java/org/sonar/plugins/dbcleaner/api/PurgeUtils.java @@ -24,6 +24,7 @@ import org.sonar.api.database.DatabaseSession; import org.sonar.api.database.model.*; import org.sonar.api.design.DependencyDto; import org.sonar.api.utils.TimeProfiler; +import org.sonar.jpa.entity.CloneBlock; import javax.persistence.Query; import java.util.List; @@ -58,6 +59,7 @@ public final class PurgeUtils { deleteSources(session, snapshotIds); deleteViolations(session, snapshotIds); deleteDependencies(session, snapshotIds); + deleteCloneBlocks(session, snapshotIds); deleteSnapshots(session, snapshotIds); } @@ -96,6 +98,13 @@ public final class PurgeUtils { executeQuery(session, "delete violations", snapshotIds, "delete from " + RuleFailureModel.class.getSimpleName() + " e where e.snapshotId in (:ids)"); } + /** + * @since 2.11 + */ + private static void deleteCloneBlocks(DatabaseSession session, List snapshotIds) { + executeQuery(session, "delete clone blocks", snapshotIds, "delete from " + CloneBlock.class.getSimpleName() + " e where e.snapshotId in (:ids)"); + } + /** * Delete SNAPSHOTS table */ diff --git a/plugins/sonar-dbcleaner-plugin/src/test/resources/org/sonar/plugins/dbcleaner/api/PurgeUtilsTest/purgeSnapshots-result.xml b/plugins/sonar-dbcleaner-plugin/src/test/resources/org/sonar/plugins/dbcleaner/api/PurgeUtilsTest/purgeSnapshots-result.xml index 912541667d8..23847972836 100644 --- a/plugins/sonar-dbcleaner-plugin/src/test/resources/org/sonar/plugins/dbcleaner/api/PurgeUtilsTest/purgeSnapshots-result.xml +++ b/plugins/sonar-dbcleaner-plugin/src/test/resources/org/sonar/plugins/dbcleaner/api/PurgeUtilsTest/purgeSnapshots-result.xml @@ -108,4 +108,7 @@ - \ No newline at end of file + + + + diff --git a/plugins/sonar-dbcleaner-plugin/src/test/resources/org/sonar/plugins/dbcleaner/api/PurgeUtilsTest/purgeSnapshots.xml b/plugins/sonar-dbcleaner-plugin/src/test/resources/org/sonar/plugins/dbcleaner/api/PurgeUtilsTest/purgeSnapshots.xml index 2b92c63361a..6f2a149c513 100644 --- a/plugins/sonar-dbcleaner-plugin/src/test/resources/org/sonar/plugins/dbcleaner/api/PurgeUtilsTest/purgeSnapshots.xml +++ b/plugins/sonar-dbcleaner-plugin/src/test/resources/org/sonar/plugins/dbcleaner/api/PurgeUtilsTest/purgeSnapshots.xml @@ -108,4 +108,7 @@ parent_dependency_id="[null]" project_snapshot_id="1" dep_usage="INHERITS" dep_weight="1" from_scope="FIL" to_scope="FIL" /> - \ No newline at end of file + + + + diff --git a/sonar-core/src/main/java/org/sonar/jpa/entity/CloneBlock.java b/sonar-core/src/main/java/org/sonar/jpa/entity/CloneBlock.java index b4de0db290a..7589a9947dd 100644 --- a/sonar-core/src/main/java/org/sonar/jpa/entity/CloneBlock.java +++ b/sonar-core/src/main/java/org/sonar/jpa/entity/CloneBlock.java @@ -25,8 +25,6 @@ import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.Table; -import org.sonar.api.database.model.ResourceModel; - /** * @since 2.11 */ @@ -44,12 +42,12 @@ public class CloneBlock { @Column(name = "snapshot_id", updatable = false, nullable = false) private Integer snapshotId; + @Column(name = "project_snapshot_id", updatable = false, nullable = false) + private Integer projectSnapshotId; + @Column(name = "hash", updatable = false, nullable = false, length = BLOCK_HASH_SIZE) private String hash; - @Column(name = "resource_key", updatable = false, nullable = false, length = ResourceModel.KEY_SIZE) - private String resourceKey; - @Column(name = "index_in_file", updatable = false, nullable = false) private Integer indexInFile; @@ -62,11 +60,11 @@ public class CloneBlock { public CloneBlock() { } - public CloneBlock(Integer snapshotId, String hash, String resourceKey, Integer indexInFile, Integer startLine, Integer endLine) { + public CloneBlock(Integer projectSnapshotId, Integer snapshotId, String hash, Integer indexInFile, Integer startLine, Integer endLine) { + this.projectSnapshotId = projectSnapshotId; this.snapshotId = snapshotId; this.hash = hash; this.indexInFile = indexInFile; - this.resourceKey = resourceKey; this.startLine = startLine; this.endLine = endLine; } @@ -79,8 +77,8 @@ public class CloneBlock { return snapshotId; } - public String getResourceKey() { - return resourceKey; + public Integer getProjectSnapshotId() { + return projectSnapshotId; } public String getHash() { diff --git a/sonar-server/src/main/webapp/WEB-INF/db/migrate/217_create_clone_blocks.rb b/sonar-server/src/main/webapp/WEB-INF/db/migrate/217_create_clone_blocks.rb index abd611c748a..4ea9a8343d1 100644 --- a/sonar-server/src/main/webapp/WEB-INF/db/migrate/217_create_clone_blocks.rb +++ b/sonar-server/src/main/webapp/WEB-INF/db/migrate/217_create_clone_blocks.rb @@ -25,16 +25,17 @@ class CreateCloneBlocks < ActiveRecord::Migration def self.up create_table :clone_blocks do |t| + t.column :project_snapshot_id, :integer, :null => false t.column :snapshot_id, :integer, :null => false t.column :hash, :string, :null => false, :limit => 50 - t.column :resource_key, :string, :null => false, :limit => 400 t.column :index_in_file, :integer, :null => false t.column :start_line, :integer, :null => false t.column :end_line, :integer, :null => false end + add_index :clone_blocks, :project_snapshot_id, :name => 'clone_blocks_project_snapshot' + add_index :clone_blocks, :snapshot_id, :name => 'clone_blocks_snapshot' add_index :clone_blocks, :hash, :name => 'clone_blocks_hash' - add_index :clone_blocks, [:snapshot_id, :resource_key], :name => 'clone_blocks_resource' end end diff --git a/sonar-testing-harness/src/main/resources/org/sonar/test/persistence/sonar-test.ddl b/sonar-testing-harness/src/main/resources/org/sonar/test/persistence/sonar-test.ddl index 1c8c3eb23fe..7ebfd7cdd4e 100644 --- a/sonar-testing-harness/src/main/resources/org/sonar/test/persistence/sonar-test.ddl +++ b/sonar-testing-harness/src/main/resources/org/sonar/test/persistence/sonar-test.ddl @@ -489,12 +489,13 @@ CREATE TABLE REVIEW_COMMENTS ( ); CREATE TABLE CLONE_BLOCKS ( + PROJECT_SNAPSHOT_ID INTEGER, SNAPSHOT_ID INTEGER, HASH VARCHAR(50), - RESOURCE_KEY VARCHAR(400), INDEX_IN_FILE INTEGER NOT NULL, START_LINE INTEGER NOT NULL, END_LINE INTEGER NOT NULL ); +CREATE INDEX CLONE_BLOCKS_PROJECT_SNAPSHOT ON CLONE_BLOCKS (PROJECT_SNAPSHOT_ID); +CREATE INDEX CLONE_BLOCKS_SNAPSHOT ON CLONE_BLOCKS (SNAPSHOT_ID); CREATE INDEX CLONE_BLOCKS_HASH ON CLONE_BLOCKS (HASH); -CREATE INDEX CLONE_BLOCKS_RESOURCE ON CLONE_BLOCKS (SNAPSHOT_ID, RESOURCE_KEY); -- cgit v1.2.3 From f0559fe73cd8d29a5f505c5a80c0c6784b97430c Mon Sep 17 00:00:00 2001 From: Fabrice Bellingard Date: Fri, 2 Sep 2011 17:12:41 +0200 Subject: SONAR-2693 Hamcrest matcher to compare a pair of translation bundles First commit and push to GitHub before activating other tests that require this code to be pushed. --- .../sonar/test/i18n/BundleSynchronizedMatcher.java | 192 +++++++++++++++++++++ .../java/org/sonar/test/i18n/I18nMatchers.java | 30 ++++ .../sonar/test/i18n/BundleSynchronizedTest.java | 121 +++++++++++++ .../test/resources/org/sonar/l10n/core.properties | 4 + .../resources/org/sonar/l10n/core_fr.properties | 4 + .../resources/org/sonar/l10n/myPlugin.properties | 4 + .../org/sonar/l10n/myPlugin_fr.properties | 4 + .../org/sonar/l10n/myPlugin_fr_CA.properties | 4 + .../org/sonar/l10n/myPlugin_fr_QB.properties | 5 + 9 files changed, 368 insertions(+) create mode 100644 sonar-testing-harness/src/main/java/org/sonar/test/i18n/BundleSynchronizedMatcher.java create mode 100644 sonar-testing-harness/src/main/java/org/sonar/test/i18n/I18nMatchers.java create mode 100644 sonar-testing-harness/src/test/java/org/sonar/test/i18n/BundleSynchronizedTest.java create mode 100644 sonar-testing-harness/src/test/resources/org/sonar/l10n/core.properties create mode 100644 sonar-testing-harness/src/test/resources/org/sonar/l10n/core_fr.properties create mode 100644 sonar-testing-harness/src/test/resources/org/sonar/l10n/myPlugin.properties create mode 100644 sonar-testing-harness/src/test/resources/org/sonar/l10n/myPlugin_fr.properties create mode 100644 sonar-testing-harness/src/test/resources/org/sonar/l10n/myPlugin_fr_CA.properties create mode 100644 sonar-testing-harness/src/test/resources/org/sonar/l10n/myPlugin_fr_QB.properties (limited to 'sonar-testing-harness/src') diff --git a/sonar-testing-harness/src/main/java/org/sonar/test/i18n/BundleSynchronizedMatcher.java b/sonar-testing-harness/src/main/java/org/sonar/test/i18n/BundleSynchronizedMatcher.java new file mode 100644 index 00000000000..1fd44ecfa6b --- /dev/null +++ b/sonar-testing-harness/src/main/java/org/sonar/test/i18n/BundleSynchronizedMatcher.java @@ -0,0 +1,192 @@ +/* + * Sonar, open source software quality management tool. + * Copyright (C) 2008-2011 SonarSource + * mailto:contact AT sonarsource DOT com + * + * Sonar 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. + * + * Sonar 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 Sonar; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.test.i18n; + +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Collection; +import java.util.Properties; +import java.util.Set; + +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.sonar.test.TestUtils; + +import com.google.common.collect.Lists; + +public class BundleSynchronizedMatcher extends BaseMatcher { + + public static final String L10N_PATH = "/org/sonar/l10n/"; + private static final String GITHUB_RAW_FILE_PATH = "https://raw.github.com/SonarSource/sonar/master/plugins/sonar-l10n-en-plugin/src/main/resources/org/sonar/l10n/"; + private static final Collection CORE_BUNDLES = Lists.newArrayList("checkstyle.properties", "core.properties", + "findbugs.properties", "gwt.properties", "pmd.properties", "squidjava.properties"); + + // we use this variable to be able to unit test this class without looking at the real Github core bundles that change all the time + private String remote_file_path; + private String bundleName; + private Collection missingKeys; + private Collection nonExistingKeys; + + public BundleSynchronizedMatcher() { + this(GITHUB_RAW_FILE_PATH); + } + + public BundleSynchronizedMatcher(String remote_file_path) { + this.remote_file_path = remote_file_path; + } + + public boolean matches(Object arg0) { + if ( !(arg0 instanceof String)) { + return false; + } + bundleName = (String) arg0; + + // Get the bundle + File bundle = getBundleFileFromClasspath(bundleName); + + // Find the default bundle name which should be compared to + String defaultBundleName = extractDefaultBundleName(bundleName); + File defaultBundle = null; + if (isCoreBundle(defaultBundleName)) { + defaultBundle = getBundleFileFromGithub(defaultBundleName); + } else { + defaultBundle = getBundleFileFromClasspath(defaultBundleName); + } + + // and now let's compare + try { + missingKeys = retrieveMissingKeys(bundle, defaultBundle); + nonExistingKeys = retrieveMissingKeys(defaultBundle, bundle); + return missingKeys.isEmpty() && nonExistingKeys.isEmpty(); + } catch (IOException e) { + fail("An error occured while reading the bundles: " + e.getMessage()); + return false; + } + } + + public void describeTo(Description description) { + StringBuilder details = new StringBuilder("\n=======================\n'"); + details.append(bundleName); + details.append("' is not synchronized."); + if ( !missingKeys.isEmpty()) { + details.append("\n\n Missing keys are:"); + for (String key : missingKeys) { + details.append("\n\t- " + key); + } + } + if ( !nonExistingKeys.isEmpty()) { + details.append("\n\nAlso, the following keys do not exist in the default bundle:"); + for (String key : nonExistingKeys) { + details.append("\n\t- " + key); + } + } + details.append("\n\n======================="); + + description.appendText(details.toString()); + } + + protected Collection retrieveMissingKeys(File bundle, File defaultBundle) throws IOException { + Collection missingKeys = Lists.newArrayList(); + + Properties bundleProps = new Properties(); + bundleProps.load(new FileInputStream(bundle)); + Set bundleKeys = bundleProps.keySet(); + + Properties defaultBundleProps = new Properties(); + defaultBundleProps.load(new FileInputStream(defaultBundle)); + Set defaultBundleKeys = defaultBundleProps.keySet(); + + for (Object key : defaultBundleKeys) { + if ( !bundleKeys.contains(key)) { + missingKeys.add(key.toString()); + } + } + + return missingKeys; + } + + protected File getBundleFileFromGithub(String defaultBundleName) { + File bundle = new File("target/l10n/download/" + defaultBundleName); + try { + saveUrl(remote_file_path + defaultBundleName, bundle); + } catch (MalformedURLException e) { + fail("Could not download the original core bundle at: " + remote_file_path + defaultBundleName); + } catch (IOException e) { + fail("Could not download the original core bundle at: " + remote_file_path + defaultBundleName); + } + assertThat("File 'target/tmp/" + defaultBundleName + "' has been downloaded but does not exist.", bundle, notNullValue()); + assertThat("File 'target/tmp/" + defaultBundleName + "' has been downloaded but does not exist.", bundle.exists(), is(true)); + return bundle; + } + + protected File getBundleFileFromClasspath(String bundleName) { + File bundle = TestUtils.getResource(L10N_PATH + bundleName); + assertThat("File '" + bundleName + "' does not exist in '/org/sonar/l10n/'.", bundle, notNullValue()); + assertThat("File '" + bundleName + "' does not exist in '/org/sonar/l10n/'.", bundle.exists(), is(true)); + return bundle; + } + + protected String extractDefaultBundleName(String bundleName) { + int firstUnderScoreIndex = bundleName.indexOf('_'); + assertThat("The bundle '" + bundleName + "' is a default bundle (without locale), so it can't be compared.", firstUnderScoreIndex > 0, + is(true)); + return bundleName.substring(0, firstUnderScoreIndex) + ".properties"; + } + + protected boolean isCoreBundle(String defaultBundleName) { + return CORE_BUNDLES.contains(defaultBundleName); + } + + private void saveUrl(String url, File localFile) throws MalformedURLException, IOException { + if (localFile.exists()) { + localFile.delete(); + } + localFile.getParentFile().mkdirs(); + + BufferedInputStream in = null; + FileOutputStream fout = null; + try { + in = new BufferedInputStream(new URL(url).openStream()); + fout = new FileOutputStream(localFile); + + byte data[] = new byte[1024]; + int count; + while ((count = in.read(data, 0, 1024)) != -1) { + fout.write(data, 0, count); + } + } finally { + if (in != null) + in.close(); + if (fout != null) + fout.close(); + } + } + +} diff --git a/sonar-testing-harness/src/main/java/org/sonar/test/i18n/I18nMatchers.java b/sonar-testing-harness/src/main/java/org/sonar/test/i18n/I18nMatchers.java new file mode 100644 index 00000000000..6d24a634eb6 --- /dev/null +++ b/sonar-testing-harness/src/main/java/org/sonar/test/i18n/I18nMatchers.java @@ -0,0 +1,30 @@ +/* + * Sonar, open source software quality management tool. + * Copyright (C) 2008-2011 SonarSource + * mailto:contact AT sonarsource DOT com + * + * Sonar 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. + * + * Sonar 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 Sonar; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.test.i18n; + +public final class I18nMatchers { + + private I18nMatchers() { + } + + public static BundleSynchronizedMatcher isBundleSynchronized() { + return new BundleSynchronizedMatcher(); + } +} diff --git a/sonar-testing-harness/src/test/java/org/sonar/test/i18n/BundleSynchronizedTest.java b/sonar-testing-harness/src/test/java/org/sonar/test/i18n/BundleSynchronizedTest.java new file mode 100644 index 00000000000..0bac5a9047b --- /dev/null +++ b/sonar-testing-harness/src/test/java/org/sonar/test/i18n/BundleSynchronizedTest.java @@ -0,0 +1,121 @@ +/* + * Sonar, open source software quality management tool. + * Copyright (C) 2008-2011 SonarSource + * mailto:contact AT sonarsource DOT com + * + * Sonar 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. + * + * Sonar 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 Sonar; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.test.i18n; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.sonar.test.i18n.I18nMatchers.isBundleSynchronized; + +import java.io.File; +import java.util.Collection; + +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.sonar.test.TestUtils; + +public class BundleSynchronizedTest { + + private BundleSynchronizedMatcher matcher; + + @Before + public void test() throws Exception { + matcher = new BundleSynchronizedMatcher("https://raw.github.com/SonarSource/sonar/master/sonar-testing-harness/src/test/resources/org/sonar/l10n/"); + } + + @Test + // The case of a Sonar plugin that embeds all the bundles for every language + public void testBundlesInsideSonarPlugin() { + // synchronized bundle + assertThat("myPlugin_fr_CA.properties", isBundleSynchronized()); + // missing keys + try { + assertThat("myPlugin_fr.properties", isBundleSynchronized()); + } catch (AssertionError e) { + assertThat(e.getMessage(), containsString("Missing keys are:\n\t- second.prop")); + } + // unnecessary many keys + try { + assertThat("myPlugin_fr_QB.properties", isBundleSynchronized()); + } catch (AssertionError e) { + assertThat(e.getMessage(), containsString("Also, the following keys do not exist in the default bundle:\n\t- fourth.prop")); + } + } + + @Test + @Ignore + // The case of a Sonar Language Pack that translates the Core bundles + public void testBundlesOfLanguagePack() { + assertThat("core_fr.properties", isBundleSynchronized()); + } + + @Test + public void testGetBundleFileFromClasspath() { + matcher.getBundleFileFromClasspath("core_fr.properties"); + try { + matcher.getBundleFileFromClasspath("unexistingBundle.properties"); + } catch (AssertionError e) { + assertThat(e.getMessage(), containsString("File 'unexistingBundle.properties' does not exist in '/org/sonar/l10n/'.")); + } + } + + @Test + @Ignore + public void testGetBundleFileFromGithub() throws Exception { + matcher.getBundleFileFromGithub("core.properties"); + assertTrue(new File("target/l10n/download/core.properties").exists()); + } + + @Test + public void testExtractDefaultBundleName() throws Exception { + assertThat(matcher.extractDefaultBundleName("myPlugin_fr.properties"), is("myPlugin.properties")); + assertThat(matcher.extractDefaultBundleName("myPlugin_fr_QB.properties"), is("myPlugin.properties")); + try { + matcher.extractDefaultBundleName("myPlugin.properties"); + } catch (AssertionError e) { + assertThat(e.getMessage(), + containsString("The bundle 'myPlugin.properties' is a default bundle (without locale), so it can't be compared.")); + } + } + + @Test + public void testIsCoreBundle() throws Exception { + assertTrue(matcher.isCoreBundle("core.properties")); + assertFalse(matcher.isCoreBundle("myPlugin.properties")); + } + + @Test + public void testRetrieveMissingKeys() throws Exception { + File defaultBundle = TestUtils.getResource(BundleSynchronizedMatcher.L10N_PATH + "myPlugin.properties"); + File frBundle = TestUtils.getResource(BundleSynchronizedMatcher.L10N_PATH + "myPlugin_fr.properties"); + File qbBundle = TestUtils.getResource(BundleSynchronizedMatcher.L10N_PATH + "myPlugin_fr_QB.properties"); + + Collection diffs = matcher.retrieveMissingKeys(frBundle, defaultBundle); + assertThat(diffs.size(), is(1)); + assertThat(diffs, hasItem("second.prop")); + + diffs = matcher.retrieveMissingKeys(qbBundle, defaultBundle); + assertThat(diffs.size(), is(0)); + } +} diff --git a/sonar-testing-harness/src/test/resources/org/sonar/l10n/core.properties b/sonar-testing-harness/src/test/resources/org/sonar/l10n/core.properties new file mode 100644 index 00000000000..d182159860d --- /dev/null +++ b/sonar-testing-harness/src/test/resources/org/sonar/l10n/core.properties @@ -0,0 +1,4 @@ +## -------- Test file for the BundleSynchronizedMatcher -------- ## +first.prop = This is my first property +second.prop = This is my second property +third.prop = This is my third property \ No newline at end of file diff --git a/sonar-testing-harness/src/test/resources/org/sonar/l10n/core_fr.properties b/sonar-testing-harness/src/test/resources/org/sonar/l10n/core_fr.properties new file mode 100644 index 00000000000..aed2acc6f0e --- /dev/null +++ b/sonar-testing-harness/src/test/resources/org/sonar/l10n/core_fr.properties @@ -0,0 +1,4 @@ +## -------- Test file for the BundleSynchronizedMatcher -------- ## +first.prop = This is my first property +#second.prop = This is my second property +third.prop = This is my third property \ No newline at end of file diff --git a/sonar-testing-harness/src/test/resources/org/sonar/l10n/myPlugin.properties b/sonar-testing-harness/src/test/resources/org/sonar/l10n/myPlugin.properties new file mode 100644 index 00000000000..d182159860d --- /dev/null +++ b/sonar-testing-harness/src/test/resources/org/sonar/l10n/myPlugin.properties @@ -0,0 +1,4 @@ +## -------- Test file for the BundleSynchronizedMatcher -------- ## +first.prop = This is my first property +second.prop = This is my second property +third.prop = This is my third property \ No newline at end of file diff --git a/sonar-testing-harness/src/test/resources/org/sonar/l10n/myPlugin_fr.properties b/sonar-testing-harness/src/test/resources/org/sonar/l10n/myPlugin_fr.properties new file mode 100644 index 00000000000..79c32ed5e81 --- /dev/null +++ b/sonar-testing-harness/src/test/resources/org/sonar/l10n/myPlugin_fr.properties @@ -0,0 +1,4 @@ +## -------- Test file for the BundleSynchronizedMatcher -------- ## +first.prop = C'est ma première propriété +#second.prop = C'est ma deuxième propriété +third.prop = C'est ma troisième propriété \ No newline at end of file diff --git a/sonar-testing-harness/src/test/resources/org/sonar/l10n/myPlugin_fr_CA.properties b/sonar-testing-harness/src/test/resources/org/sonar/l10n/myPlugin_fr_CA.properties new file mode 100644 index 00000000000..462cc8bcf36 --- /dev/null +++ b/sonar-testing-harness/src/test/resources/org/sonar/l10n/myPlugin_fr_CA.properties @@ -0,0 +1,4 @@ +## -------- Test file for the BundleSynchronizedMatcher -------- ## +first.prop = C'est ma première propriété +second.prop = C'est ma deuxième propriété +third.prop = C'est ma troisième propriété \ No newline at end of file diff --git a/sonar-testing-harness/src/test/resources/org/sonar/l10n/myPlugin_fr_QB.properties b/sonar-testing-harness/src/test/resources/org/sonar/l10n/myPlugin_fr_QB.properties new file mode 100644 index 00000000000..5076dd808c8 --- /dev/null +++ b/sonar-testing-harness/src/test/resources/org/sonar/l10n/myPlugin_fr_QB.properties @@ -0,0 +1,5 @@ +## -------- Test file for the BundleSynchronizedMatcher -------- ## +first.prop = C'est ma première propriété +second.prop = C'est ma deuxième propriété +third.prop = C'est ma troisième propriété +fourth.prop = C'est ma quatrième propriété \ No newline at end of file -- cgit v1.2.3 From 7df9484047b3d1d58d5b142e4042728a01512026 Mon Sep 17 00:00:00 2001 From: Fabrice Bellingard Date: Fri, 2 Sep 2011 18:03:38 +0200 Subject: SONAR-2693 Improve unit tests for I18n Harmcrest matcher --- .../sonar/test/i18n/BundleSynchronizedMatcher.java | 23 +++++++++++++++++++++- .../sonar/test/i18n/BundleSynchronizedTest.java | 21 ++++++++++++++------ .../resources/org/sonar/l10n/core_fr_CA.properties | 4 ++++ 3 files changed, 41 insertions(+), 7 deletions(-) create mode 100644 sonar-testing-harness/src/test/resources/org/sonar/l10n/core_fr_CA.properties (limited to 'sonar-testing-harness/src') diff --git a/sonar-testing-harness/src/main/java/org/sonar/test/i18n/BundleSynchronizedMatcher.java b/sonar-testing-harness/src/main/java/org/sonar/test/i18n/BundleSynchronizedMatcher.java index 1fd44ecfa6b..18351b9c70b 100644 --- a/sonar-testing-harness/src/main/java/org/sonar/test/i18n/BundleSynchronizedMatcher.java +++ b/sonar-testing-harness/src/main/java/org/sonar/test/i18n/BundleSynchronizedMatcher.java @@ -28,6 +28,7 @@ import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; +import java.io.FileWriter; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; @@ -35,6 +36,7 @@ import java.util.Collection; import java.util.Properties; import java.util.Set; +import org.apache.commons.io.IOUtils; import org.hamcrest.BaseMatcher; import org.hamcrest.Description; import org.sonar.test.TestUtils; @@ -102,16 +104,35 @@ public class BundleSynchronizedMatcher extends BaseMatcher { } } if ( !nonExistingKeys.isEmpty()) { - details.append("\n\nAlso, the following keys do not exist in the default bundle:"); + details.append("\n\nThe following keys do not exist in the default bundle:"); for (String key : nonExistingKeys) { details.append("\n\t- " + key); } } details.append("\n\n======================="); + printReport(details.toString()); + description.appendText(details.toString()); } + private void printReport(String details) { + File dumpFile = new File("target/l10n/" + bundleName + ".report.txt"); + if (dumpFile.exists()) { + dumpFile.delete(); + } + dumpFile.getParentFile().mkdirs(); + FileWriter writer = null; + try { + writer = new FileWriter(dumpFile); + writer.write(details); + } catch (IOException e) { + System.out.println("Unable to write the report to 'target/l10n/" + bundleName + ".report.txt'."); + } finally { + IOUtils.closeQuietly(writer); + } + } + protected Collection retrieveMissingKeys(File bundle, File defaultBundle) throws IOException { Collection missingKeys = Lists.newArrayList(); diff --git a/sonar-testing-harness/src/test/java/org/sonar/test/i18n/BundleSynchronizedTest.java b/sonar-testing-harness/src/test/java/org/sonar/test/i18n/BundleSynchronizedTest.java index 0bac5a9047b..73d6e310689 100644 --- a/sonar-testing-harness/src/test/java/org/sonar/test/i18n/BundleSynchronizedTest.java +++ b/sonar-testing-harness/src/test/java/org/sonar/test/i18n/BundleSynchronizedTest.java @@ -31,17 +31,17 @@ import java.io.File; import java.util.Collection; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.sonar.test.TestUtils; public class BundleSynchronizedTest { + private static final String GITHUB_RAW_FILE_PATH = "https://raw.github.com/SonarSource/sonar/master/sonar-testing-harness/src/test/resources/org/sonar/l10n/"; private BundleSynchronizedMatcher matcher; @Before public void test() throws Exception { - matcher = new BundleSynchronizedMatcher("https://raw.github.com/SonarSource/sonar/master/sonar-testing-harness/src/test/resources/org/sonar/l10n/"); + matcher = new BundleSynchronizedMatcher(); } @Test @@ -49,25 +49,34 @@ public class BundleSynchronizedTest { public void testBundlesInsideSonarPlugin() { // synchronized bundle assertThat("myPlugin_fr_CA.properties", isBundleSynchronized()); + assertFalse(new File("target/l10n/myPlugin_fr_CA.properties.report.txt").exists()); // missing keys try { assertThat("myPlugin_fr.properties", isBundleSynchronized()); + assertTrue(new File("target/l10n/myPlugin_fr.properties.report.txt").exists()); } catch (AssertionError e) { assertThat(e.getMessage(), containsString("Missing keys are:\n\t- second.prop")); } // unnecessary many keys try { assertThat("myPlugin_fr_QB.properties", isBundleSynchronized()); + assertTrue(new File("target/l10n/myPlugin_fr_QB.properties.report.txt").exists()); } catch (AssertionError e) { - assertThat(e.getMessage(), containsString("Also, the following keys do not exist in the default bundle:\n\t- fourth.prop")); + assertThat(e.getMessage(), containsString("The following keys do not exist in the default bundle:\n\t- fourth.prop")); } } @Test - @Ignore // The case of a Sonar Language Pack that translates the Core bundles public void testBundlesOfLanguagePack() { - assertThat("core_fr.properties", isBundleSynchronized()); + // synchronized bundle + assertThat("core_fr_CA.properties", new BundleSynchronizedMatcher(GITHUB_RAW_FILE_PATH)); + // missing keys + try { + assertThat("core_fr.properties", new BundleSynchronizedMatcher(GITHUB_RAW_FILE_PATH)); + } catch (AssertionError e) { + assertThat(e.getMessage(), containsString("Missing keys are:\n\t- second.prop")); + } } @Test @@ -81,8 +90,8 @@ public class BundleSynchronizedTest { } @Test - @Ignore public void testGetBundleFileFromGithub() throws Exception { + matcher = new BundleSynchronizedMatcher(GITHUB_RAW_FILE_PATH); matcher.getBundleFileFromGithub("core.properties"); assertTrue(new File("target/l10n/download/core.properties").exists()); } diff --git a/sonar-testing-harness/src/test/resources/org/sonar/l10n/core_fr_CA.properties b/sonar-testing-harness/src/test/resources/org/sonar/l10n/core_fr_CA.properties new file mode 100644 index 00000000000..d182159860d --- /dev/null +++ b/sonar-testing-harness/src/test/resources/org/sonar/l10n/core_fr_CA.properties @@ -0,0 +1,4 @@ +## -------- Test file for the BundleSynchronizedMatcher -------- ## +first.prop = This is my first property +second.prop = This is my second property +third.prop = This is my third property \ No newline at end of file -- cgit v1.2.3 From 66dcfc8adc7b36d31e0fd80353066a2d6938cd98 Mon Sep 17 00:00:00 2001 From: Fabrice Bellingard Date: Mon, 5 Sep 2011 09:47:06 +0200 Subject: SONAR-2693 A bit of refactoring --- .../sonar/test/i18n/BundleSynchronizedMatcher.java | 23 +++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) (limited to 'sonar-testing-harness/src') diff --git a/sonar-testing-harness/src/main/java/org/sonar/test/i18n/BundleSynchronizedMatcher.java b/sonar-testing-harness/src/main/java/org/sonar/test/i18n/BundleSynchronizedMatcher.java index 18351b9c70b..8d794b01dfc 100644 --- a/sonar-testing-harness/src/main/java/org/sonar/test/i18n/BundleSynchronizedMatcher.java +++ b/sonar-testing-harness/src/main/java/org/sonar/test/i18n/BundleSynchronizedMatcher.java @@ -94,6 +94,18 @@ public class BundleSynchronizedMatcher extends BaseMatcher { } public void describeTo(Description description) { + // report file + File dumpFile = new File("target/l10n/" + bundleName + ".report.txt"); + + // prepare message + StringBuilder details = prepareDetailsMessage(dumpFile); + description.appendText(details.toString()); + + // print report in target directory + printReport(dumpFile, details.toString()); + } + + private StringBuilder prepareDetailsMessage(File dumpFile) { StringBuilder details = new StringBuilder("\n=======================\n'"); details.append(bundleName); details.append("' is not synchronized."); @@ -109,15 +121,12 @@ public class BundleSynchronizedMatcher extends BaseMatcher { details.append("\n\t- " + key); } } - details.append("\n\n======================="); - - printReport(details.toString()); - - description.appendText(details.toString()); + details.append("\n\nSee report file located at: " + dumpFile.getAbsolutePath()); + details.append("\n======================="); + return details; } - private void printReport(String details) { - File dumpFile = new File("target/l10n/" + bundleName + ".report.txt"); + private void printReport(File dumpFile, String details) { if (dumpFile.exists()) { dumpFile.delete(); } -- cgit v1.2.3 From cf9829861fca004f21640c27d692adb064b0f098 Mon Sep 17 00:00:00 2001 From: Fabrice Bellingard Date: Thu, 8 Sep 2011 14:21:19 +0200 Subject: SONAR-2693 Improve I18n Harmcrest matcher - #isBundleSynchronized replace by #isBundleUpToDate - #assertAllBundlesUpToDate static method added to avoid specifying files one by one - if bundles are Core bundles translations, then the original bundles are searched on Github using the Sonar version specified by the "sonar.version" property in the POM --- .../sonar/test/i18n/BundleSynchronizedMatcher.java | 35 +++++++++++++--- .../java/org/sonar/test/i18n/I18nMatchers.java | 47 +++++++++++++++++++++- .../sonar/test/i18n/BundleSynchronizedTest.java | 20 +++++++-- .../src/test/resources/version.properties | 2 + 4 files changed, 93 insertions(+), 11 deletions(-) create mode 100644 sonar-testing-harness/src/test/resources/version.properties (limited to 'sonar-testing-harness/src') diff --git a/sonar-testing-harness/src/main/java/org/sonar/test/i18n/BundleSynchronizedMatcher.java b/sonar-testing-harness/src/main/java/org/sonar/test/i18n/BundleSynchronizedMatcher.java index 8d794b01dfc..23baf7771d5 100644 --- a/sonar-testing-harness/src/main/java/org/sonar/test/i18n/BundleSynchronizedMatcher.java +++ b/sonar-testing-harness/src/main/java/org/sonar/test/i18n/BundleSynchronizedMatcher.java @@ -33,7 +33,9 @@ import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.util.Collection; +import java.util.MissingResourceException; import java.util.Properties; +import java.util.ResourceBundle; import java.util.Set; import org.apache.commons.io.IOUtils; @@ -163,17 +165,38 @@ public class BundleSynchronizedMatcher extends BaseMatcher { } protected File getBundleFileFromGithub(String defaultBundleName) { - File bundle = new File("target/l10n/download/" + defaultBundleName); + File localBundle = new File("target/l10n/download/" + defaultBundleName); try { - saveUrl(remote_file_path + defaultBundleName, bundle); + String sonarVersion = getSonarVersionFromBundle(); + String remoteFile = computeGitHubURL(defaultBundleName, sonarVersion); + saveUrlToLocalFile(remoteFile, localBundle); } catch (MalformedURLException e) { fail("Could not download the original core bundle at: " + remote_file_path + defaultBundleName); } catch (IOException e) { fail("Could not download the original core bundle at: " + remote_file_path + defaultBundleName); } - assertThat("File 'target/tmp/" + defaultBundleName + "' has been downloaded but does not exist.", bundle, notNullValue()); - assertThat("File 'target/tmp/" + defaultBundleName + "' has been downloaded but does not exist.", bundle.exists(), is(true)); - return bundle; + assertThat("File 'target/tmp/" + defaultBundleName + "' has been downloaded but does not exist.", localBundle, notNullValue()); + assertThat("File 'target/tmp/" + defaultBundleName + "' has been downloaded but does not exist.", localBundle.exists(), is(true)); + return localBundle; + } + + protected String computeGitHubURL(String defaultBundleName, String sonarVersion) { + String computedURL = remote_file_path + defaultBundleName; + if (sonarVersion != null && !sonarVersion.startsWith("${") && !sonarVersion.contains("-SNAPSHOT")) { + computedURL = computedURL.replace("/master/", "/" + sonarVersion + "/"); + } + return computedURL; + } + + protected String getSonarVersionFromBundle() { + String version = null; + try { + ResourceBundle bundle = ResourceBundle.getBundle("version"); + version = bundle.getString("sonar.version"); + } catch (MissingResourceException e) { + // no problem, we won't use any specific version + } + return version; } protected File getBundleFileFromClasspath(String bundleName) { @@ -194,7 +217,7 @@ public class BundleSynchronizedMatcher extends BaseMatcher { return CORE_BUNDLES.contains(defaultBundleName); } - private void saveUrl(String url, File localFile) throws MalformedURLException, IOException { + private void saveUrlToLocalFile(String url, File localFile) throws MalformedURLException, IOException { if (localFile.exists()) { localFile.delete(); } diff --git a/sonar-testing-harness/src/main/java/org/sonar/test/i18n/I18nMatchers.java b/sonar-testing-harness/src/main/java/org/sonar/test/i18n/I18nMatchers.java index 6d24a634eb6..90bc31e817e 100644 --- a/sonar-testing-harness/src/main/java/org/sonar/test/i18n/I18nMatchers.java +++ b/sonar-testing-harness/src/main/java/org/sonar/test/i18n/I18nMatchers.java @@ -19,12 +19,57 @@ */ package org.sonar.test.i18n; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; + +import java.io.File; +import java.util.Collection; +import java.util.Map; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang.StringUtils; +import org.sonar.test.TestUtils; + +import com.google.common.collect.Maps; + public final class I18nMatchers { private I18nMatchers() { } - public static BundleSynchronizedMatcher isBundleSynchronized() { + public static BundleSynchronizedMatcher isBundleUpToDate() { return new BundleSynchronizedMatcher(); } + + public static void assertAllBundlesUpToDate() { + File bundleFolder = TestUtils.getResource(BundleSynchronizedMatcher.L10N_PATH); + if (bundleFolder == null || !bundleFolder.isDirectory()) { + fail("No bundle found in '" + BundleSynchronizedMatcher.L10N_PATH + "'"); + } + + Collection bundles = FileUtils.listFiles(bundleFolder, new String[] { "properties" }, false); + Map failedAssertionMessages = Maps.newHashMap(); + for (File bundle : bundles) { + String bundleName = bundle.getName(); + if (bundleName.indexOf('_') > 0) { + try { + assertThat(bundleName, isBundleUpToDate()); + } catch (AssertionError e) { + failedAssertionMessages.put(bundleName, e.getMessage()); + } + } + } + + if ( !failedAssertionMessages.isEmpty()) { + StringBuilder message = new StringBuilder(); + message.append(failedAssertionMessages.size()); + message.append(" bundles are not up-to-date: "); + message.append(StringUtils.join(failedAssertionMessages.keySet(), ", ")); + message.append("\n\n"); + message.append(StringUtils.join(failedAssertionMessages.values(), "\n\n")); + fail(message.toString()); + } + + } + } diff --git a/sonar-testing-harness/src/test/java/org/sonar/test/i18n/BundleSynchronizedTest.java b/sonar-testing-harness/src/test/java/org/sonar/test/i18n/BundleSynchronizedTest.java index 73d6e310689..3edc655ddfd 100644 --- a/sonar-testing-harness/src/test/java/org/sonar/test/i18n/BundleSynchronizedTest.java +++ b/sonar-testing-harness/src/test/java/org/sonar/test/i18n/BundleSynchronizedTest.java @@ -25,7 +25,7 @@ import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; -import static org.sonar.test.i18n.I18nMatchers.isBundleSynchronized; +import static org.sonar.test.i18n.I18nMatchers.isBundleUpToDate; import java.io.File; import java.util.Collection; @@ -48,18 +48,18 @@ public class BundleSynchronizedTest { // The case of a Sonar plugin that embeds all the bundles for every language public void testBundlesInsideSonarPlugin() { // synchronized bundle - assertThat("myPlugin_fr_CA.properties", isBundleSynchronized()); + assertThat("myPlugin_fr_CA.properties", isBundleUpToDate()); assertFalse(new File("target/l10n/myPlugin_fr_CA.properties.report.txt").exists()); // missing keys try { - assertThat("myPlugin_fr.properties", isBundleSynchronized()); + assertThat("myPlugin_fr.properties", isBundleUpToDate()); assertTrue(new File("target/l10n/myPlugin_fr.properties.report.txt").exists()); } catch (AssertionError e) { assertThat(e.getMessage(), containsString("Missing keys are:\n\t- second.prop")); } // unnecessary many keys try { - assertThat("myPlugin_fr_QB.properties", isBundleSynchronized()); + assertThat("myPlugin_fr_QB.properties", isBundleUpToDate()); assertTrue(new File("target/l10n/myPlugin_fr_QB.properties.report.txt").exists()); } catch (AssertionError e) { assertThat(e.getMessage(), containsString("The following keys do not exist in the default bundle:\n\t- fourth.prop")); @@ -127,4 +127,16 @@ public class BundleSynchronizedTest { diffs = matcher.retrieveMissingKeys(qbBundle, defaultBundle); assertThat(diffs.size(), is(0)); } + + @Test + public void testComputeGitHubURL() throws Exception { + assertThat( + matcher.computeGitHubURL("core.properties", null), + is("https://raw.github.com/SonarSource/sonar/master/plugins/sonar-l10n-en-plugin/src/main/resources/org/sonar/l10n/core.properties")); + assertThat( + matcher.computeGitHubURL("core.properties", "2.11-SNAPSHOT"), + is("https://raw.github.com/SonarSource/sonar/master/plugins/sonar-l10n-en-plugin/src/main/resources/org/sonar/l10n/core.properties")); + assertThat(matcher.computeGitHubURL("core.properties", "2.10"), + is("https://raw.github.com/SonarSource/sonar/2.10/plugins/sonar-l10n-en-plugin/src/main/resources/org/sonar/l10n/core.properties")); + } } diff --git a/sonar-testing-harness/src/test/resources/version.properties b/sonar-testing-harness/src/test/resources/version.properties new file mode 100644 index 00000000000..f57ea3a66e4 --- /dev/null +++ b/sonar-testing-harness/src/test/resources/version.properties @@ -0,0 +1,2 @@ +# The version here in this test file does not matter as the Harmcrest matcher will always look at master if there's a "-SNAPSHOT" suffix +sonar.version=2.11-SNAPSHOT \ No newline at end of file -- cgit v1.2.3 From a92d0f3a983b33794bbd26ff674eab21ec34541a Mon Sep 17 00:00:00 2001 From: Fabrice Bellingard Date: Mon, 12 Sep 2011 17:40:54 +0200 Subject: SONAR-2693 Make it possible to pass the sonar version to the matcher - In order to remove the magic of getting the version of Sonar via the POM - Documentation also added on the I18nMatchers static methods --- .../sonar/test/i18n/BundleSynchronizedMatcher.java | 24 +++--------- .../java/org/sonar/test/i18n/I18nMatchers.java | 45 ++++++++++++++++++++-- .../sonar/test/i18n/BundleSynchronizedTest.java | 8 ++-- .../src/test/resources/version.properties | 2 - 4 files changed, 52 insertions(+), 27 deletions(-) delete mode 100644 sonar-testing-harness/src/test/resources/version.properties (limited to 'sonar-testing-harness/src') diff --git a/sonar-testing-harness/src/main/java/org/sonar/test/i18n/BundleSynchronizedMatcher.java b/sonar-testing-harness/src/main/java/org/sonar/test/i18n/BundleSynchronizedMatcher.java index 23baf7771d5..4a1e077ca8d 100644 --- a/sonar-testing-harness/src/main/java/org/sonar/test/i18n/BundleSynchronizedMatcher.java +++ b/sonar-testing-harness/src/main/java/org/sonar/test/i18n/BundleSynchronizedMatcher.java @@ -33,9 +33,7 @@ import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.util.Collection; -import java.util.MissingResourceException; import java.util.Properties; -import java.util.ResourceBundle; import java.util.Set; import org.apache.commons.io.IOUtils; @@ -52,17 +50,19 @@ public class BundleSynchronizedMatcher extends BaseMatcher { private static final Collection CORE_BUNDLES = Lists.newArrayList("checkstyle.properties", "core.properties", "findbugs.properties", "gwt.properties", "pmd.properties", "squidjava.properties"); + private String sonarVersion; // we use this variable to be able to unit test this class without looking at the real Github core bundles that change all the time private String remote_file_path; private String bundleName; private Collection missingKeys; private Collection nonExistingKeys; - public BundleSynchronizedMatcher() { - this(GITHUB_RAW_FILE_PATH); + public BundleSynchronizedMatcher(String sonarVersion) { + this(sonarVersion, GITHUB_RAW_FILE_PATH); } - public BundleSynchronizedMatcher(String remote_file_path) { + public BundleSynchronizedMatcher(String sonarVersion, String remote_file_path) { + this.sonarVersion = sonarVersion; this.remote_file_path = remote_file_path; } @@ -167,7 +167,6 @@ public class BundleSynchronizedMatcher extends BaseMatcher { protected File getBundleFileFromGithub(String defaultBundleName) { File localBundle = new File("target/l10n/download/" + defaultBundleName); try { - String sonarVersion = getSonarVersionFromBundle(); String remoteFile = computeGitHubURL(defaultBundleName, sonarVersion); saveUrlToLocalFile(remoteFile, localBundle); } catch (MalformedURLException e) { @@ -182,23 +181,12 @@ public class BundleSynchronizedMatcher extends BaseMatcher { protected String computeGitHubURL(String defaultBundleName, String sonarVersion) { String computedURL = remote_file_path + defaultBundleName; - if (sonarVersion != null && !sonarVersion.startsWith("${") && !sonarVersion.contains("-SNAPSHOT")) { + if (sonarVersion != null && !sonarVersion.contains("-SNAPSHOT")) { computedURL = computedURL.replace("/master/", "/" + sonarVersion + "/"); } return computedURL; } - protected String getSonarVersionFromBundle() { - String version = null; - try { - ResourceBundle bundle = ResourceBundle.getBundle("version"); - version = bundle.getString("sonar.version"); - } catch (MissingResourceException e) { - // no problem, we won't use any specific version - } - return version; - } - protected File getBundleFileFromClasspath(String bundleName) { File bundle = TestUtils.getResource(L10N_PATH + bundleName); assertThat("File '" + bundleName + "' does not exist in '/org/sonar/l10n/'.", bundle, notNullValue()); diff --git a/sonar-testing-harness/src/main/java/org/sonar/test/i18n/I18nMatchers.java b/sonar-testing-harness/src/main/java/org/sonar/test/i18n/I18nMatchers.java index 90bc31e817e..6fc8862995b 100644 --- a/sonar-testing-harness/src/main/java/org/sonar/test/i18n/I18nMatchers.java +++ b/sonar-testing-harness/src/main/java/org/sonar/test/i18n/I18nMatchers.java @@ -37,11 +37,43 @@ public final class I18nMatchers { private I18nMatchers() { } + /** + * Returns a matcher which checks that a translation bundle is up to date with the corresponding English Core bundle. + *
    + *
  • If a version of Sonar is specified, then the check is done against this version of the bundle found on Sonar Github repository.
  • + *
  • If sonarVersion is set to NULL, the check is done against the latest version of this bundle found on Github (master branch).
  • + *
+ * + * @param sonarVersion + * the version of the bundle to check against, or NULL to check against the latest source on GitHub + * @return the matcher + */ + public static BundleSynchronizedMatcher isBundleUpToDate(String sonarVersion) { + return new BundleSynchronizedMatcher(sonarVersion); + } + + /** + * Returns a matcher which checks that a translation bundle is up to date with the corresponding default one found in the same folder.
+ *
+ * This matcher is used for Sonar plugins that embed their own translations. + * + * @return the matcher + */ public static BundleSynchronizedMatcher isBundleUpToDate() { - return new BundleSynchronizedMatcher(); + return new BundleSynchronizedMatcher(null); } - public static void assertAllBundlesUpToDate() { + /** + * Checks that all the Core translation bundles found on the classpath are up to date with the corresponding English ones. + *
    + *
  • If a version of Sonar is specified, then the check is done against this version of the bundles found on Sonar Github repository.
  • + *
  • If sonarVersion is set to NULL, the check is done against the latest version of this bundles found on Github (master branch).
  • + *
+ * + * @param sonarVersion + * the version of the bundles to check against, or NULL to check against the latest source on GitHub + */ + public static void assertAllBundlesUpToDate(String sonarVersion) { File bundleFolder = TestUtils.getResource(BundleSynchronizedMatcher.L10N_PATH); if (bundleFolder == null || !bundleFolder.isDirectory()) { fail("No bundle found in '" + BundleSynchronizedMatcher.L10N_PATH + "'"); @@ -53,7 +85,7 @@ public final class I18nMatchers { String bundleName = bundle.getName(); if (bundleName.indexOf('_') > 0) { try { - assertThat(bundleName, isBundleUpToDate()); + assertThat(bundleName, isBundleUpToDate(sonarVersion)); } catch (AssertionError e) { failedAssertionMessages.put(bundleName, e.getMessage()); } @@ -69,7 +101,14 @@ public final class I18nMatchers { message.append(StringUtils.join(failedAssertionMessages.values(), "\n\n")); fail(message.toString()); } + } + /** + * Checks that all the translation bundles found on the classpath are up to date with the corresponding default one found in the same + * folder. + */ + public static void assertAllBundlesUpToDate() { + assertAllBundlesUpToDate(null); } } diff --git a/sonar-testing-harness/src/test/java/org/sonar/test/i18n/BundleSynchronizedTest.java b/sonar-testing-harness/src/test/java/org/sonar/test/i18n/BundleSynchronizedTest.java index 3edc655ddfd..9eee351687a 100644 --- a/sonar-testing-harness/src/test/java/org/sonar/test/i18n/BundleSynchronizedTest.java +++ b/sonar-testing-harness/src/test/java/org/sonar/test/i18n/BundleSynchronizedTest.java @@ -41,7 +41,7 @@ public class BundleSynchronizedTest { @Before public void test() throws Exception { - matcher = new BundleSynchronizedMatcher(); + matcher = new BundleSynchronizedMatcher(null); } @Test @@ -70,10 +70,10 @@ public class BundleSynchronizedTest { // The case of a Sonar Language Pack that translates the Core bundles public void testBundlesOfLanguagePack() { // synchronized bundle - assertThat("core_fr_CA.properties", new BundleSynchronizedMatcher(GITHUB_RAW_FILE_PATH)); + assertThat("core_fr_CA.properties", new BundleSynchronizedMatcher(null, GITHUB_RAW_FILE_PATH)); // missing keys try { - assertThat("core_fr.properties", new BundleSynchronizedMatcher(GITHUB_RAW_FILE_PATH)); + assertThat("core_fr.properties", new BundleSynchronizedMatcher(null, GITHUB_RAW_FILE_PATH)); } catch (AssertionError e) { assertThat(e.getMessage(), containsString("Missing keys are:\n\t- second.prop")); } @@ -91,7 +91,7 @@ public class BundleSynchronizedTest { @Test public void testGetBundleFileFromGithub() throws Exception { - matcher = new BundleSynchronizedMatcher(GITHUB_RAW_FILE_PATH); + matcher = new BundleSynchronizedMatcher(null, GITHUB_RAW_FILE_PATH); matcher.getBundleFileFromGithub("core.properties"); assertTrue(new File("target/l10n/download/core.properties").exists()); } diff --git a/sonar-testing-harness/src/test/resources/version.properties b/sonar-testing-harness/src/test/resources/version.properties deleted file mode 100644 index f57ea3a66e4..00000000000 --- a/sonar-testing-harness/src/test/resources/version.properties +++ /dev/null @@ -1,2 +0,0 @@ -# The version here in this test file does not matter as the Harmcrest matcher will always look at master if there's a "-SNAPSHOT" suffix -sonar.version=2.11-SNAPSHOT \ No newline at end of file -- cgit v1.2.3 From 038f9a0b69d405235238565f35e4525c966b0712 Mon Sep 17 00:00:00 2001 From: Simon Brandhof Date: Fri, 16 Sep 2011 16:32:00 +0200 Subject: SONAR-2693 the list of missing translations must be copyable and must contain english values --- .../sonar/test/i18n/BundleSynchronizedMatcher.java | 94 +++++++++++----------- .../sonar/test/i18n/BundleSynchronizedTest.java | 13 +-- 2 files changed, 54 insertions(+), 53 deletions(-) (limited to 'sonar-testing-harness/src') diff --git a/sonar-testing-harness/src/main/java/org/sonar/test/i18n/BundleSynchronizedMatcher.java b/sonar-testing-harness/src/main/java/org/sonar/test/i18n/BundleSynchronizedMatcher.java index 4a1e077ca8d..5ad95967561 100644 --- a/sonar-testing-harness/src/main/java/org/sonar/test/i18n/BundleSynchronizedMatcher.java +++ b/sonar-testing-harness/src/main/java/org/sonar/test/i18n/BundleSynchronizedMatcher.java @@ -19,29 +19,22 @@ */ package org.sonar.test.i18n; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.notNullValue; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.fail; - -import java.io.BufferedInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.FileWriter; -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.Collection; -import java.util.Properties; -import java.util.Set; - +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; import org.apache.commons.io.IOUtils; import org.hamcrest.BaseMatcher; import org.hamcrest.Description; import org.sonar.test.TestUtils; -import com.google.common.collect.Lists; +import java.io.*; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.*; + +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; public class BundleSynchronizedMatcher extends BaseMatcher { @@ -54,8 +47,8 @@ public class BundleSynchronizedMatcher extends BaseMatcher { // we use this variable to be able to unit test this class without looking at the real Github core bundles that change all the time private String remote_file_path; private String bundleName; - private Collection missingKeys; - private Collection nonExistingKeys; + private SortedMap missingKeys; + private SortedMap nonExistingKeys; public BundleSynchronizedMatcher(String sonarVersion) { this(sonarVersion, GITHUB_RAW_FILE_PATH); @@ -67,7 +60,7 @@ public class BundleSynchronizedMatcher extends BaseMatcher { } public boolean matches(Object arg0) { - if ( !(arg0 instanceof String)) { + if (!(arg0 instanceof String)) { return false; } bundleName = (String) arg0; @@ -86,8 +79,8 @@ public class BundleSynchronizedMatcher extends BaseMatcher { // and now let's compare try { - missingKeys = retrieveMissingKeys(bundle, defaultBundle); - nonExistingKeys = retrieveMissingKeys(defaultBundle, bundle); + missingKeys = retrieveMissingTranslations(bundle, defaultBundle); + nonExistingKeys = retrieveMissingTranslations(defaultBundle, bundle); return missingKeys.isEmpty() && nonExistingKeys.isEmpty(); } catch (IOException e) { fail("An error occured while reading the bundles: " + e.getMessage()); @@ -111,23 +104,22 @@ public class BundleSynchronizedMatcher extends BaseMatcher { StringBuilder details = new StringBuilder("\n=======================\n'"); details.append(bundleName); details.append("' is not synchronized."); - if ( !missingKeys.isEmpty()) { - details.append("\n\n Missing keys are:"); - for (String key : missingKeys) { - details.append("\n\t- " + key); - } - } - if ( !nonExistingKeys.isEmpty()) { - details.append("\n\nThe following keys do not exist in the default bundle:"); - for (String key : nonExistingKeys) { - details.append("\n\t- " + key); - } - } + print("\n\n Missing translations are:", missingKeys, details); + print("\n\nThe following translations do not exist in the reference bundle:", nonExistingKeys, details); details.append("\n\nSee report file located at: " + dumpFile.getAbsolutePath()); details.append("\n======================="); return details; } + private void print(String title, SortedMap translations, StringBuilder to) { + if (!translations.isEmpty()) { + to.append(title); + for (Map.Entry entry : translations.entrySet()) { + to.append("\n").append(entry.getKey()).append("=").append(entry.getValue()); + } + } + } + private void printReport(File dumpFile, String details) { if (dumpFile.exists()) { dumpFile.delete(); @@ -144,26 +136,34 @@ public class BundleSynchronizedMatcher extends BaseMatcher { } } - protected Collection retrieveMissingKeys(File bundle, File defaultBundle) throws IOException { - Collection missingKeys = Lists.newArrayList(); - - Properties bundleProps = new Properties(); - bundleProps.load(new FileInputStream(bundle)); - Set bundleKeys = bundleProps.keySet(); + protected SortedMap retrieveMissingTranslations(File bundle, File referenceBundle) throws IOException { + SortedMap missingKeys = Maps.newTreeMap(); - Properties defaultBundleProps = new Properties(); - defaultBundleProps.load(new FileInputStream(defaultBundle)); - Set defaultBundleKeys = defaultBundleProps.keySet(); + Properties bundleProps = loadProperties(bundle); + Properties referenceProperties = loadProperties(referenceBundle); - for (Object key : defaultBundleKeys) { - if ( !bundleKeys.contains(key)) { - missingKeys.add(key.toString()); + for (Map.Entry entry : referenceProperties.entrySet()) { + String key = (String) entry.getKey(); + if (!bundleProps.containsKey(key)) { + missingKeys.put(key, (String) entry.getValue()); } } return missingKeys; } + private Properties loadProperties(File f) throws IOException { + Properties props = new Properties(); + FileInputStream input = new FileInputStream(f); + try { + props.load(input); + return props; + + } finally { + IOUtils.closeQuietly(input); + } + } + protected File getBundleFileFromGithub(String defaultBundleName) { File localBundle = new File("target/l10n/download/" + defaultBundleName); try { diff --git a/sonar-testing-harness/src/test/java/org/sonar/test/i18n/BundleSynchronizedTest.java b/sonar-testing-harness/src/test/java/org/sonar/test/i18n/BundleSynchronizedTest.java index 9eee351687a..6568ff16101 100644 --- a/sonar-testing-harness/src/test/java/org/sonar/test/i18n/BundleSynchronizedTest.java +++ b/sonar-testing-harness/src/test/java/org/sonar/test/i18n/BundleSynchronizedTest.java @@ -29,6 +29,7 @@ import static org.sonar.test.i18n.I18nMatchers.isBundleUpToDate; import java.io.File; import java.util.Collection; +import java.util.SortedMap; import org.junit.Before; import org.junit.Test; @@ -55,14 +56,14 @@ public class BundleSynchronizedTest { assertThat("myPlugin_fr.properties", isBundleUpToDate()); assertTrue(new File("target/l10n/myPlugin_fr.properties.report.txt").exists()); } catch (AssertionError e) { - assertThat(e.getMessage(), containsString("Missing keys are:\n\t- second.prop")); + assertThat(e.getMessage(), containsString("Missing translations are:\nsecond.prop")); } // unnecessary many keys try { assertThat("myPlugin_fr_QB.properties", isBundleUpToDate()); assertTrue(new File("target/l10n/myPlugin_fr_QB.properties.report.txt").exists()); } catch (AssertionError e) { - assertThat(e.getMessage(), containsString("The following keys do not exist in the default bundle:\n\t- fourth.prop")); + assertThat(e.getMessage(), containsString("The following translations do not exist in the reference bundle:\nfourth.prop")); } } @@ -75,7 +76,7 @@ public class BundleSynchronizedTest { try { assertThat("core_fr.properties", new BundleSynchronizedMatcher(null, GITHUB_RAW_FILE_PATH)); } catch (AssertionError e) { - assertThat(e.getMessage(), containsString("Missing keys are:\n\t- second.prop")); + assertThat(e.getMessage(), containsString("Missing translations are:\nsecond.prop")); } } @@ -120,11 +121,11 @@ public class BundleSynchronizedTest { File frBundle = TestUtils.getResource(BundleSynchronizedMatcher.L10N_PATH + "myPlugin_fr.properties"); File qbBundle = TestUtils.getResource(BundleSynchronizedMatcher.L10N_PATH + "myPlugin_fr_QB.properties"); - Collection diffs = matcher.retrieveMissingKeys(frBundle, defaultBundle); + SortedMap diffs = matcher.retrieveMissingTranslations(frBundle, defaultBundle); assertThat(diffs.size(), is(1)); - assertThat(diffs, hasItem("second.prop")); + assertThat(diffs.keySet(), hasItem("second.prop")); - diffs = matcher.retrieveMissingKeys(qbBundle, defaultBundle); + diffs = matcher.retrieveMissingTranslations(qbBundle, defaultBundle); assertThat(diffs.size(), is(0)); } -- cgit v1.2.3