http://git-wip-us.apache.org/repos/asf/sentry/blob/9351d19d/sentry-provider/sentry-provider-db/src/test/java/org/apache/sentry/service/thrift/TestFullUpdateInitializer.java ---------------------------------------------------------------------- diff --git a/sentry-provider/sentry-provider-db/src/test/java/org/apache/sentry/service/thrift/TestFullUpdateInitializer.java b/sentry-provider/sentry-provider-db/src/test/java/org/apache/sentry/service/thrift/TestFullUpdateInitializer.java new file mode 100644 index 0000000..589acbe --- /dev/null +++ b/sentry-provider/sentry-provider-db/src/test/java/org/apache/sentry/service/thrift/TestFullUpdateInitializer.java @@ -0,0 +1,346 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sentry.service.thrift; + +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.hive.metastore.HiveMetaStoreClient; +import org.apache.hadoop.hive.metastore.api.Database; +import org.apache.hadoop.hive.metastore.api.MetaException; +import org.apache.hadoop.hive.metastore.api.Partition; +import org.apache.hadoop.hive.metastore.api.StorageDescriptor; +import org.apache.hadoop.hive.metastore.api.Table; +import org.apache.thrift.TException; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class TestFullUpdateInitializer { + + private static Configuration conf = new Configuration(); + + static { + conf.setInt(org.apache.sentry.hdfs.ServiceConstants.ServerConfig + .SENTRY_HDFS_SYNC_METASTORE_CACHE_MAX_PART_PER_RPC, 1); + conf.setInt(org.apache.sentry.hdfs.ServiceConstants.ServerConfig + .SENTRY_HDFS_SYNC_METASTORE_CACHE_MAX_TABLES_PER_RPC, 1); + conf.setInt(org.apache.sentry.hdfs.ServiceConstants.ServerConfig + .SENTRY_HDFS_SYNC_METASTORE_CACHE_INIT_THREADS, 8); + } + + /** + * Representation of a Hive table. A table has a name and a list of partitions. + */ + private static class HiveTable { + String name; + List<String> partitions; + + HiveTable(String name) { + this.name = name; + this.partitions = new ArrayList<>(); + } + + HiveTable(String name, List<String> partitions) { + this.name = name; + this.partitions = partitions; + if (this.partitions == null) { + this.partitions = new ArrayList<>(); + } + } + + HiveTable add(String partition) { + partitions.add(partition); + return this; + } + } + + /** + * Representation of a Hive database. A database has a name and a list of tables + */ + private static class HiveDb { + String name; + Collection<HiveTable> tables; + + HiveDb(String name) { + this.name = name; + tables = new ArrayList<>(); + } + + HiveDb(String name, Collection<HiveTable> tables) { + this.name = name; + this.tables = tables; + if (this.tables == null) { + this.tables = new ArrayList<>(); + } + } + + void add(HiveTable table) { + this.tables.add(table); + } + } + + /** + * Representation of a full Hive snapshot. A snapshot is collection of databases + */ + private static class HiveSnapshot { + List<HiveDb> databases = new ArrayList<>(); + + HiveSnapshot() { + } + + HiveSnapshot(Collection<HiveDb> dblist) { + if (dblist != null) { + databases.addAll(dblist); + } + } + + HiveSnapshot add(HiveDb db) { + this.databases.add(db); + return this; + } + } + + /** + * Convert Hive snapshot to mock client that will return proper values + * for the snapshot. + */ + private static class MockClient { + HiveMetaStoreClient client; + + MockClient(HiveSnapshot snapshot) throws TException { + client = Mockito.mock(HiveMetaStoreClient.class); + List<String> dbNames = new ArrayList<>(snapshot.databases.size()); + // Walk over all databases and mock appropriate objects + for (HiveDb mdb: snapshot.databases) { + String dbName = mdb.name; + dbNames.add(dbName); + Database db = makeDb(dbName); + Mockito.when(client.getDatabase(dbName)).thenReturn(db); + List<String> tableNames = new ArrayList<>(mdb.tables.size()); + // Walk over all tables for the database and mock appropriate objects + for (HiveTable table: mdb.tables) { + String tableName = table.name; + tableNames.add(tableName); + Table mockTable = makeTable(dbName, tableName); + Mockito.when(client.getTableObjectsByName(dbName, + Lists.newArrayList(tableName))) + .thenReturn(Lists.newArrayList(mockTable)); + Mockito.when(client.listPartitionNames(dbName, tableName, (short) -1)) + .thenReturn(table.partitions); + // Walk across all partitions and mock appropriate objects + for (String partName: table.partitions) { + Partition p = makePartition(dbName, tableName, partName); + Mockito.when(client.getPartitionsByNames(dbName, tableName, + Lists.<String>newArrayList(partName))) + .thenReturn(Lists.<Partition>newArrayList(p)); + } + } + Mockito.when(client.getAllTables(dbName)).thenReturn(tableNames); + } + // Return all database names + Mockito.when(client.getAllDatabases()).thenReturn(dbNames); + } + } + + private static class MockHMSClientFactory implements HiveConnectionFactory { + + private final HiveMetaStoreClient mClient; + + private MockHMSClientFactory(MockClient mClient) { + this.mClient = mClient.client; + } + + private MockHMSClientFactory(HiveMetaStoreClient client) { + this.mClient = client; + } + + @Override + public HMSClient connect() throws IOException, InterruptedException, MetaException { + return new HMSClient(mClient); + } + + @Override + public void close() throws Exception { + } + } + + /** + * Create mock database with the given name + * @param name Database name + * @return Mock database object + */ + private static Database makeDb(String name) { + Database db = Mockito.mock(Database.class); + Mockito.when(db.getName()).thenReturn(name); + Mockito.when(db.getLocationUri()).thenReturn("hdfs:///" + name); + return db; + } + + /** + * Create mock table + * @param dbName db for this table + * @param tableName name of the table + * @return mock table object + */ + private static Table makeTable(String dbName, String tableName) { + Table table = Mockito.mock(Table.class); + Mockito.when(table.getDbName()).thenReturn(dbName); + Mockito.when(table.getTableName()).thenReturn(tableName); + StorageDescriptor sd = Mockito.mock(StorageDescriptor.class); + Mockito.when(sd.getLocation()).thenReturn( + String.format("hdfs:///%s/%s", dbName, tableName)); + Mockito.when(table.getSd()).thenReturn(sd); + return table; + } + + /** + * Create mock partition + * @param dbName database for this partition + * @param tableName table for this partition + * @param partName partition name + * @return mock partition object + */ + private static Partition makePartition(String dbName, String tableName, String partName) { + Partition partition = Mockito.mock(Partition.class); + StorageDescriptor sd = Mockito.mock(StorageDescriptor.class); + Mockito.when(sd.getLocation()).thenReturn( + String.format("hdfs:///%s/%s/%s", dbName, tableName, partName)); + Mockito.when(partition.getSd()).thenReturn(sd); + return partition; + } + + @Test + // Test basic operation with small database + public void testSimple() throws Exception { + HiveTable tab21 = new HiveTable("tab21"); + HiveTable tab31 = new HiveTable("tab31").add("part311").add("part312"); + HiveDb db3 = new HiveDb("db3", Lists.newArrayList(tab31)); + HiveDb db2 = new HiveDb("db2", Lists.newArrayList(tab21)); + HiveDb db1 = new HiveDb("db1"); + HiveSnapshot snap = new HiveSnapshot().add(db1).add(db2).add(db3); + MockClient c = new MockClient(snap); + + Map<String, Collection<String>> update; + try(FullUpdateInitializer cacheInitializer = + new FullUpdateInitializer(new MockHMSClientFactory(c), conf)) { + update = cacheInitializer.getFullHMSSnapshot(); + } + Assert.assertEquals(5, update.size()); + Assert.assertEquals(Sets.newHashSet("db1"), update.get("db1")); + Assert.assertEquals(Sets.newHashSet("db2"), update.get("db2")); + Assert.assertEquals(Sets.newHashSet("db3"), update.get("db3")); + Assert.assertEquals(Sets.newHashSet("db2/tab21"), update.get("db2.tab21")); + Assert.assertEquals(Sets.newHashSet("db3/tab31", + "db3/tab31/part311", "db3/tab31/part312"), update.get("db3.tab31")); + } + + @Test + // Test that invalid paths are handled correctly + public void testInvalidPaths() throws Exception { + //Set up mocks: db1.tb1, with tb1 returning a wrong dbname (db2) + Database db1 = makeDb("db1"); + + Table tab1 = Mockito.mock(Table.class); + //Return a wrong db name, so that this triggers an exception + Mockito.when(tab1.getDbName()).thenReturn("db2"); + Mockito.when(tab1.getTableName()).thenReturn("tab1"); + + HiveMetaStoreClient client = Mockito.mock(HiveMetaStoreClient.class); + Mockito.when(client.getAllDatabases()).thenReturn(Lists.newArrayList("db1")); + Mockito.when(client.getDatabase("db1")).thenReturn(db1); + + Table tab12 = Mockito.mock(Table.class); + Mockito.when(tab12.getDbName()).thenReturn("db1"); + Mockito.when(tab12.getTableName()).thenReturn("tab21"); + StorageDescriptor sd21 = Mockito.mock(StorageDescriptor.class); + Mockito.when(sd21.getLocation()).thenReturn("hdfs:///db1/tab21"); + Mockito.when(tab12.getSd()).thenReturn(sd21); + + Mockito.when(client.getTableObjectsByName("db1", + Lists.newArrayList("tab1"))).thenReturn(Lists.newArrayList(tab1)); + Mockito.when(client.getTableObjectsByName("db1", + Lists.newArrayList("tab12"))).thenReturn(Lists.newArrayList(tab12)); + Mockito.when(client.getAllTables("db1")). + thenReturn(Lists.newArrayList("tab1", "tab12")); + + + Map<String, Collection<String>> update; + try(FullUpdateInitializer cacheInitializer = + new FullUpdateInitializer(new MockHMSClientFactory(client), conf)) { + update = cacheInitializer.getFullHMSSnapshot(); + } + Assert.assertEquals(2, update.size()); + Assert.assertEquals(Sets.newHashSet("db1"), update.get("db1")); + Assert.assertEquals(Sets.newHashSet("db1/tab21"), update.get("db1.tab21")); + } + + @Test + // Test handling of a big tables and partitions + public void testBig() throws Exception { + int ndbs = 3; + int ntables = 51; + int nparts = 131; + + HiveSnapshot snap = new HiveSnapshot(); + + for (int i = 0; i < ndbs; i++) { + HiveDb db = new HiveDb("db" + i); + for (int j = 0; j < ntables; j++) { + HiveTable table = new HiveTable("table" + i + j); + for (int k = 0; k < nparts; k++) { + table.add("part" + i + j + k); + } + db.add(table); + } + snap.add(db); + } + MockClient c = new MockClient(snap); + Map<String, Collection<String>> update; + try(FullUpdateInitializer cacheInitializer = + new FullUpdateInitializer(new MockHMSClientFactory(c), conf)) { + update = cacheInitializer.getFullHMSSnapshot(); + } + Assert.assertEquals((ntables * ndbs) + ndbs, update.size()); + for (int i = 0; i < ndbs; i++) { + String dbName = "db" + i; + Assert.assertEquals(Sets.newHashSet(dbName), update.get(dbName)); + + for (int j = 0; j < ntables; j++) { + String tableName = "table" + i + j; + Set<String> values = new HashSet<>(); + values.add(String.format("%s/%s", dbName, tableName)); + for (int k = 0; k < nparts; k++) { + String partName = "part" + i + j + k; + values.add(String.format("%s/%s/%s", dbName, tableName, partName)); + } + String authz = dbName + "." + tableName; + Assert.assertEquals(values, update.get(authz)); + } + } + } + +}
http://git-wip-us.apache.org/repos/asf/sentry/blob/9351d19d/sentry-provider/sentry-provider-db/src/test/java/org/apache/sentry/service/thrift/TestFullUpdateModifier.java ---------------------------------------------------------------------- diff --git a/sentry-provider/sentry-provider-db/src/test/java/org/apache/sentry/service/thrift/TestFullUpdateModifier.java b/sentry-provider/sentry-provider-db/src/test/java/org/apache/sentry/service/thrift/TestFullUpdateModifier.java new file mode 100644 index 0000000..c6be80d --- /dev/null +++ b/sentry-provider/sentry-provider-db/src/test/java/org/apache/sentry/service/thrift/TestFullUpdateModifier.java @@ -0,0 +1,482 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.sentry.service.thrift; + +import org.apache.hadoop.hive.metastore.api.NotificationEvent; +import org.apache.hadoop.hive.metastore.api.Partition; +import org.apache.hadoop.hive.metastore.api.StorageDescriptor; +import org.apache.hadoop.hive.metastore.api.Table; +import org.apache.hadoop.hive.metastore.messaging.MessageDeserializer; +import org.apache.sentry.binding.metastore.messaging.json.SentryJSONAddPartitionMessage; +import org.apache.sentry.binding.metastore.messaging.json.SentryJSONAlterPartitionMessage; +import org.apache.sentry.binding.metastore.messaging.json.SentryJSONAlterTableMessage; +import org.apache.sentry.binding.metastore.messaging.json.SentryJSONCreateDatabaseMessage; +import org.apache.sentry.binding.metastore.messaging.json.SentryJSONCreateTableMessage; +import org.apache.sentry.binding.metastore.messaging.json.SentryJSONDropDatabaseMessage; +import org.apache.sentry.binding.metastore.messaging.json.SentryJSONDropPartitionMessage; +import org.apache.sentry.binding.metastore.messaging.json.SentryJSONDropTableMessage; +import org.apache.sentry.binding.metastore.messaging.json.SentryJSONMessageDeserializer; +import org.junit.Test; +import org.mockito.Mockito; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import static org.apache.hadoop.hive.metastore.messaging.EventMessage.EventType.*; +import static org.junit.Assert.*; + +public class TestFullUpdateModifier { + private static final String SERVER = "s"; + private static final String PRINCIPAL = "p"; + private static final String DB = "Db1"; + private static final String TABLE = "Tab1"; + private static final String AUTH = DB.toLowerCase() + "." + TABLE.toLowerCase(); + private static final String PATH = "foo/bar"; + private static final String LOCATION = uri(PATH); + + private static final Table TABLE_OBJ = new Table(TABLE, DB, "", 0, 0, 0, + buildStorageDescriptor(LOCATION), null, null, "", "", ""); + + /** + * Convert path to HDFS URI + */ + private static final String uri(String path) { + return "hdfs:///" + path; + } + + /** + * Creates a StorageDescriptor using the location as parameter. + * + * @param location The location string for the StorageDescriptor + * @return A StorageDescriptor object + */ + private static StorageDescriptor buildStorageDescriptor(String location) { + return new StorageDescriptor(null, location, "", "", false, 0, null, null, null, null); + } + + /** + * Creates a Table object using the db name, table name and table location as parameters. + * + * @param dbName The database name string. + * @param tableName The table name string. + * @param location The table location string. + * @return A Table object + */ + private static Table buildTable(String dbName, String tableName, String location) { + return new Table(tableName, dbName, "", 0, 0, 0, + buildStorageDescriptor(location), null, null, "", "", ""); + } + + /** + * Test create database event. It should add database and its location. + * As a result we should have entry {"db1": {foo/bar}} + * @throws Exception + */ + @Test + public void testCreateDatabase() throws Exception { + Map<String, Collection<String>> update = new HashMap<>(); + NotificationEvent event = new NotificationEvent(0, 0, CREATE_DATABASE.toString(), ""); + MessageDeserializer deserializer = Mockito.mock(SentryJSONMessageDeserializer.class); + + SentryJSONCreateDatabaseMessage message = + new SentryJSONCreateDatabaseMessage(SERVER, PRINCIPAL, DB, 0L, LOCATION); + Mockito.when(deserializer.getCreateDatabaseMessage("")).thenReturn(message); + FullUpdateModifier.applyEvent(update, event, deserializer); + Map<String, Set<String>> expected = new HashMap<>(); + expected.put(DB.toLowerCase(), Collections.singleton(PATH)); + assertEquals(expected, update); + } + + /** + * Test drop database event. It should drop database record. + * @throws Exception + */ + @Test + public void testDropDatabase() throws Exception { + Map<String, Collection<String>> update = new HashMap<>(); + update.put(DB.toLowerCase(), Collections.singleton(PATH)); + NotificationEvent event = new NotificationEvent(0, 0, DROP_DATABASE.toString(), ""); + MessageDeserializer deserializer = Mockito.mock(SentryJSONMessageDeserializer.class); + + SentryJSONDropDatabaseMessage message = + new SentryJSONDropDatabaseMessage(SERVER, PRINCIPAL, DB, 0L, LOCATION); + Mockito.when(deserializer.getDropDatabaseMessage("")).thenReturn(message); + FullUpdateModifier.applyEvent(update, event, deserializer); + assertTrue(update.isEmpty()); + } + + /** + * Test drop database event when dropped database location doesn't + * match original database location. Should leave update intact. + * @throws Exception + */ + @Test + public void testDropDatabaseWrongLocation() throws Exception { + Map<String, Collection<String>> update = new HashMap<>(); + update.put(DB.toLowerCase(), Collections.singleton(PATH)); + + NotificationEvent event = new NotificationEvent(0, 0, DROP_DATABASE.toString(), ""); + MessageDeserializer deserializer = Mockito.mock(SentryJSONMessageDeserializer.class); + + SentryJSONDropDatabaseMessage message = + new SentryJSONDropDatabaseMessage(SERVER, PRINCIPAL, DB, 0L, + "hdfs:///bad/location"); + Mockito.when(deserializer.getDropDatabaseMessage("")).thenReturn(message); + FullUpdateModifier.applyEvent(update, event, deserializer); + // DB should stay + Map<String, Set<String>> expected = new HashMap<>(); + expected.put(DB.toLowerCase(), Collections.singleton(PATH)); + assertEquals(expected, update); + } + + /** + * Test drop database which has tables/partitions. + * Should drop all reated database records but leave unrelated records in place. + * @throws Exception + */ + @Test + public void testDropDatabaseWithTables() throws Exception { + Map<String, Collection<String>> update = new HashMap<>(); + update.put(DB.toLowerCase(), Collections.singleton(PATH)); + update.put(AUTH, Collections.singleton(PATH)); + update.put("unrelated", Collections.singleton(PATH)); + NotificationEvent event = new NotificationEvent(0, 0, DROP_DATABASE.toString(), ""); + MessageDeserializer deserializer = Mockito.mock(SentryJSONMessageDeserializer.class); + + SentryJSONDropDatabaseMessage message = + new SentryJSONDropDatabaseMessage(SERVER, PRINCIPAL, DB, 0L, LOCATION); + Mockito.when(deserializer.getDropDatabaseMessage("")).thenReturn(message); + FullUpdateModifier.applyEvent(update, event, deserializer); + Map<String, Set<String>> expected = new HashMap<>(); + expected.put("unrelated", Collections.singleton(PATH)); + assertEquals(expected, update); + } + + /** + * Test create table event. It should add table and its location. + * As a result we should have entry {"db1.tab1": {foo/bar}} + * @throws Exception + */ + @Test + public void testCreateTable() throws Exception { + Map<String, Collection<String>> update = new HashMap<>(); + NotificationEvent event = new NotificationEvent(0, 0, CREATE_TABLE.toString(), ""); + MessageDeserializer deserializer = Mockito.mock(SentryJSONMessageDeserializer.class); + + SentryJSONCreateTableMessage message = + new SentryJSONCreateTableMessage(SERVER, PRINCIPAL, TABLE_OBJ, Collections.emptyIterator(), 0L); + Mockito.when(deserializer.getCreateTableMessage("")).thenReturn(message); + FullUpdateModifier.applyEvent(update, event, deserializer); + Map<String, Set<String>> expected = new HashMap<>(); + expected.put(AUTH, Collections.singleton(PATH)); + assertEquals(expected, update); + } + + /** + * Test drop table event. It should drop table record. + * @throws Exception + */ + @Test + public void testDropTable() throws Exception { + Map<String, Collection<String>> update = new HashMap<>(); + update.put(AUTH, Collections.singleton(PATH)); + NotificationEvent event = new NotificationEvent(0, 0, DROP_TABLE.toString(), ""); + MessageDeserializer deserializer = Mockito.mock(SentryJSONMessageDeserializer.class); + + SentryJSONDropTableMessage message = + new SentryJSONDropTableMessage(SERVER, PRINCIPAL, DB, TABLE, 0L, LOCATION); + Mockito.when(deserializer.getDropTableMessage("")).thenReturn(message); + FullUpdateModifier.applyEvent(update, event, deserializer); + assertTrue(update.isEmpty()); + } + + /** + * Test drop table event. It should drop table record. + * @throws Exception + */ + @Test + public void testDropTableWrongLocation() throws Exception { + Map<String, Collection<String>> update = new HashMap<>(); + update.put(AUTH, Collections.singleton(PATH)); + NotificationEvent event = new NotificationEvent(0, 0, DROP_TABLE.toString(), ""); + MessageDeserializer deserializer = Mockito.mock(SentryJSONMessageDeserializer.class); + + SentryJSONDropTableMessage message = + new SentryJSONDropTableMessage(SERVER, PRINCIPAL, DB, TABLE, 0L, + "hdfs:///bad/location"); + Mockito.when(deserializer.getDropTableMessage("")).thenReturn(message); + FullUpdateModifier.applyEvent(update, event, deserializer); + // DB should stay + assertEquals(Collections.singleton(PATH), update.get(AUTH)); + assertEquals(1, update.size()); + } + + /** + * Test add partition event. It should add table and its location. + * As a result we should have entry {"db1.tab1": {foo/bar, hello/world}} + * @throws Exception + */ + @Test + public void testAddPartition() throws Exception { + Map<String, Collection<String>> update = new HashMap<>(); + Set<String> locations = new HashSet<>(); + locations.add(PATH); + update.put(AUTH, locations); + + NotificationEvent event = new NotificationEvent(0, 0, ADD_PARTITION.toString(), ""); + MessageDeserializer deserializer = Mockito.mock(SentryJSONMessageDeserializer.class); + + String partPath = "hello/world"; + String partLocation = uri(partPath); + + SentryJSONAddPartitionMessage message = + new SentryJSONAddPartitionMessage(SERVER, PRINCIPAL, TABLE_OBJ, + Collections.emptyIterator(), Collections.emptyIterator(), + 0L, Collections.singletonList(partLocation)); + Mockito.when(deserializer.getAddPartitionMessage("")).thenReturn(message); + FullUpdateModifier.applyEvent(update, event, deserializer); + Set<String> expected = new HashSet<>(2); + expected.add(PATH); + expected.add(partPath); + assertEquals(expected, update.get(AUTH)); + } + + /** + * Test drop partition event. It should drop partition info from the list of locations. + * @throws Exception + */ + @Test + public void testDropPartitions() throws Exception { + String partPath = "hello/world"; + String partLocation = uri(partPath); + Map<String, Collection<String>> update = new HashMap<>(); + Set<String> locations = new HashSet<>(); + locations.add(PATH); + locations.add(partPath); + update.put(AUTH, locations); + + NotificationEvent event = new NotificationEvent(0, 0, DROP_PARTITION.toString(), ""); + MessageDeserializer deserializer = Mockito.mock(SentryJSONMessageDeserializer.class); + + SentryJSONDropPartitionMessage message = + new SentryJSONDropPartitionMessage(SERVER, PRINCIPAL, TABLE_OBJ, + Collections.<Map<String,String>>emptyList(), 0L, Collections.singletonList(partLocation)); + Mockito.when(deserializer.getDropPartitionMessage("")).thenReturn(message); + FullUpdateModifier.applyEvent(update, event, deserializer); + assertEquals(Collections.singleton(PATH), update.get(AUTH)); + } + + /** + * Test alter partition event. It should change partition location + * @throws Exception + */ + @Test + public void testAlterPartition() throws Exception { + String partPath = "hello/world"; + String partLocation = uri(partPath); + + String newPath = "better/world"; + String newLocation = uri(newPath); + + Map<String, Collection<String>> update = new HashMap<>(); + Set<String> locations = new HashSet<>(); + locations.add(PATH); + locations.add(partPath); + update.put(AUTH, locations); + + NotificationEvent event = new NotificationEvent(0, 0, ALTER_PARTITION.toString(), ""); + MessageDeserializer deserializer = Mockito.mock(SentryJSONMessageDeserializer.class); + + Partition partitionObjBefore = new Partition(null, DB, TABLE, 0, 0, buildStorageDescriptor(partLocation), null); + Partition partitionObjAfter = new Partition(null, DB, TABLE, 0, 0, buildStorageDescriptor(newLocation), null); + + SentryJSONAlterPartitionMessage message = + new SentryJSONAlterPartitionMessage(SERVER, PRINCIPAL, TABLE_OBJ, + partitionObjBefore, partitionObjAfter, 0L); + + Mockito.when(deserializer.getAlterPartitionMessage("")).thenReturn(message); + FullUpdateModifier.applyEvent(update, event, deserializer); + + Set<String> expected = new HashSet<>(2); + expected.add(PATH); + expected.add(newPath); + assertEquals(expected, update.get(AUTH)); + } + + /** + * Test alter table event that changes database name when there are no tables. + * @throws Exception + */ + @Test + public void testAlterTableChangeDbNameNoTables() throws Exception { + Map<String, Collection<String>> update = new HashMap<>(); + update.put(DB.toLowerCase(), Collections.singleton(PATH)); + String newDbName = "Db2"; + + NotificationEvent event = new NotificationEvent(0, 0, ALTER_TABLE.toString(), ""); + event.setDbName(newDbName); + event.setTableName(TABLE); + + MessageDeserializer deserializer = Mockito.mock(SentryJSONMessageDeserializer.class); + + SentryJSONAlterTableMessage message = + new SentryJSONAlterTableMessage(SERVER, PRINCIPAL, TABLE_OBJ, TABLE_OBJ, 0L); + + Mockito.when(deserializer.getAlterTableMessage("")).thenReturn(message); + FullUpdateModifier.applyEvent(update, event, deserializer); + assertEquals(Collections.singleton(PATH), update.get(newDbName.toLowerCase())); + assertFalse(update.containsKey(DB.toLowerCase())); + } + + @Test + /** + * Test alter table event that changes database name when there are tables. + * All entries like "dbName.tableName" should have dbName changed to the new name. + * @throws Exception + */ + public void testAlterTableChangeDbNameWithTables() throws Exception { + Map<String, Collection<String>> update = new HashMap<>(); + update.put(DB.toLowerCase(), Collections.singleton(PATH)); + Set<String> locations = new HashSet<>(1); + locations.add(PATH); + update.put(AUTH, locations); + + String newDbName = "Db2"; + String newAuth = newDbName.toLowerCase() + "." + TABLE.toLowerCase(); + + NotificationEvent event = new NotificationEvent(0, 0, ALTER_TABLE.toString(), ""); + event.setDbName(newDbName); + event.setTableName(TABLE); + + MessageDeserializer deserializer = Mockito.mock(SentryJSONMessageDeserializer.class); + + SentryJSONAlterTableMessage message = + new SentryJSONAlterTableMessage(SERVER, PRINCIPAL, TABLE_OBJ, TABLE_OBJ, 0L); + + Mockito.when(deserializer.getAlterTableMessage("")).thenReturn(message); + FullUpdateModifier.applyEvent(update, event, deserializer); + Map<String, Set<String>> expected = new HashMap<>(2); + expected.put(newDbName.toLowerCase(), Collections.singleton(PATH)); + expected.put(newAuth, Collections.singleton(PATH)); + assertEquals(expected, update); + } + + /** + * Test alter table event that changes table name. + * @throws Exception + */ + @Test + public void testAlterTableChangeTableName() throws Exception { + Map<String, Collection<String>> update = new HashMap<>(); + update.put(DB.toLowerCase(), Collections.singleton(PATH)); + Set<String> locations = new HashSet<>(1); + locations.add(PATH); + update.put(AUTH, locations); + + String newTableName = "Table2"; + String newAuth = DB.toLowerCase() + "." + newTableName.toLowerCase(); + + NotificationEvent event = new NotificationEvent(0, 0, ALTER_TABLE.toString(), ""); + event.setDbName(DB); + event.setTableName(newTableName); + + MessageDeserializer deserializer = Mockito.mock(SentryJSONMessageDeserializer.class); + + SentryJSONAlterTableMessage message = + new SentryJSONAlterTableMessage(SERVER, PRINCIPAL, TABLE_OBJ, TABLE_OBJ, 0L); + + Mockito.when(deserializer.getAlterTableMessage("")).thenReturn(message); + FullUpdateModifier.applyEvent(update, event, deserializer); + Map<String, Set<String>> expected = new HashMap<>(2); + expected.put(DB.toLowerCase(), Collections.singleton(PATH)); + expected.put(newAuth, Collections.singleton(PATH)); + assertEquals(expected, update); + } + + /** + * Test alter table event that changes object location. + * @throws Exception + */ + @Test + public void testAlterTableChangeLocation() throws Exception { + Map<String, Collection<String>> update = new HashMap<>(); + update.put(DB.toLowerCase(), Collections.singleton(PATH)); + Set<String> locations = new HashSet<>(1); + locations.add(PATH); + update.put(AUTH, locations); + + NotificationEvent event = new NotificationEvent(0, 0, ALTER_TABLE.toString(), ""); + event.setDbName(DB); + event.setTableName(TABLE); + + String newPath = "hello/world"; + String newLocation = uri(newPath); + + MessageDeserializer deserializer = Mockito.mock(SentryJSONMessageDeserializer.class); + + Table tableWithNewLocation = buildTable(DB, TABLE, newLocation); + SentryJSONAlterTableMessage message = + new SentryJSONAlterTableMessage(SERVER, PRINCIPAL, TABLE_OBJ, tableWithNewLocation, 0L); + + Mockito.when(deserializer.getAlterTableMessage("")).thenReturn(message); + FullUpdateModifier.applyEvent(update, event, deserializer); + Map<String, Set<String>> expected = new HashMap<>(2); + expected.put(DB.toLowerCase(), Collections.singleton(PATH)); + expected.put(AUTH.toLowerCase(), Collections.singleton(newPath)); + assertEquals(expected, update); + } + + /** + * Test renamePrefixKeys function. + * We ask to rename "foo.bar" key to "foo.baz" key. + * @throws Exception + */ + @Test + public void testRenamePrefixKeys() throws Exception { + String oldKey = "foo."; + String newKey = "baz."; + String postfix = "bar"; + Map<String, Collection<String>> update = new HashMap<>(); + update.put(oldKey + postfix , Collections.<String>emptySet()); + FullUpdateModifier.renamePrefixKeys(update, oldKey, newKey); + assertEquals(1, update.size()); + assertTrue(update.containsKey(newKey + postfix)); + } + + /** + * Test renamePostfixKeys and RenamePrefixKeys functions mwhen the destination keys exist. + * Should nto change anything. + * We ask to rename "foo.bar" key to "baz.bar" key. + * @throws Exception + */ + @Test + public void testRenameKeysWithConflicts() throws Exception { + Map<String, Collection<String>> update = new HashMap<>(); + update.put("foo.bar", Collections.<String>emptySet()); + update.put("baz.bar", Collections.<String>emptySet()); + Map<String, Collection<String>> expected = new HashMap<>(update); + + FullUpdateModifier.renamePrefixKeys(update, "foo.", "baz."); + assertEquals(update, expected); + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/sentry/blob/9351d19d/sentry-provider/sentry-provider-db/src/test/java/org/apache/sentry/service/thrift/TestHiveNotificationFetcher.java ---------------------------------------------------------------------- diff --git a/sentry-provider/sentry-provider-db/src/test/java/org/apache/sentry/service/thrift/TestHiveNotificationFetcher.java b/sentry-provider/sentry-provider-db/src/test/java/org/apache/sentry/service/thrift/TestHiveNotificationFetcher.java new file mode 100644 index 0000000..83a1bec --- /dev/null +++ b/sentry-provider/sentry-provider-db/src/test/java/org/apache/sentry/service/thrift/TestHiveNotificationFetcher.java @@ -0,0 +1,163 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + <p> + http://www.apache.org/licenses/LICENSE-2.0 + <p> + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +package org.apache.sentry.service.thrift; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.apache.hadoop.hive.metastore.HiveMetaStoreClient; +import org.apache.hadoop.hive.metastore.IMetaStoreClient.NotificationFilter; +import org.apache.hadoop.hive.metastore.api.NotificationEvent; +import org.apache.hadoop.hive.metastore.api.NotificationEventResponse; +import org.apache.sentry.hdfs.UniquePathsUpdate; +import org.apache.sentry.provider.db.service.persistent.SentryStore; +import org.junit.Test; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +public class TestHiveNotificationFetcher { + @Test + public void testGetEmptyNotificationsWhenHmsReturnsANullResponse() throws Exception { + SentryStore store = Mockito.mock(SentryStore.class); + HiveConnectionFactory hmsConnection = Mockito.mock(HiveConnectionFactory.class); + HiveMetaStoreClient hmsClient = Mockito.mock(HiveMetaStoreClient.class); + + Mockito.when(hmsConnection.connect()).thenReturn(new HMSClient(hmsClient)); + + try (HiveNotificationFetcher fetcher = new HiveNotificationFetcher(store, hmsConnection)) { + List<NotificationEvent> events; + + Mockito.when(hmsClient.getNextNotification(0, Integer.MAX_VALUE, null)) + .thenReturn(null); + + events = fetcher.fetchNotifications(0); + assertTrue(events.isEmpty()); + } + } + + @Test + public void testGetEmptyNotificationsWhenHmsReturnsEmptyEvents() throws Exception { + SentryStore store = Mockito.mock(SentryStore.class); + HiveConnectionFactory hmsConnection = Mockito.mock(HiveConnectionFactory.class); + HiveMetaStoreClient hmsClient = Mockito.mock(HiveMetaStoreClient.class); + + Mockito.when(hmsConnection.connect()).thenReturn(new HMSClient(hmsClient)); + + try (HiveNotificationFetcher fetcher = new HiveNotificationFetcher(store, hmsConnection)) { + List<NotificationEvent> events; + + Mockito.when(hmsClient.getNextNotification(0, Integer.MAX_VALUE, null)) + .thenReturn(new NotificationEventResponse(Collections.<NotificationEvent>emptyList())); + + events = fetcher.fetchNotifications(0); + assertTrue(events.isEmpty()); + } + } + + @Test + public void testGetAllNotificationsReturnedByHms() throws Exception { + SentryStore store = Mockito.mock(SentryStore.class); + HiveConnectionFactory hmsConnection = Mockito.mock(HiveConnectionFactory.class); + HiveMetaStoreClient hmsClient = Mockito.mock(HiveMetaStoreClient.class); + + Mockito.when(hmsConnection.connect()).thenReturn(new HMSClient(hmsClient)); + + try (HiveNotificationFetcher fetcher = new HiveNotificationFetcher(store, hmsConnection)) { + List<NotificationEvent> events; + + Mockito.when(hmsClient.getNextNotification(0, Integer.MAX_VALUE, null)) + .thenReturn(new NotificationEventResponse( + Arrays.<NotificationEvent>asList( + new NotificationEvent(1L, 0, "CREATE_DATABASE", ""), + new NotificationEvent(2L, 0, "CREATE_TABLE", "") + ) + )); + + events = fetcher.fetchNotifications(0); + assertEquals(2, events.size()); + assertEquals(1, events.get(0).getEventId()); + assertEquals("CREATE_DATABASE", events.get(0).getEventType()); + assertEquals(2, events.get(1).getEventId()); + assertEquals("CREATE_TABLE", events.get(1).getEventType()); + } + } + + @Test + public void testGetDuplicatedEventsAndFilterEventsAlreadySeen() throws Exception { + final SentryStore store = Mockito.mock(SentryStore.class); + HiveConnectionFactory hmsConnection = Mockito.mock(HiveConnectionFactory.class); + HiveMetaStoreClient hmsClient = Mockito.mock(HiveMetaStoreClient.class); + + Mockito.when(hmsConnection.connect()).thenReturn(new HMSClient(hmsClient)); + + try (HiveNotificationFetcher fetcher = new HiveNotificationFetcher(store, hmsConnection)) { + List<NotificationEvent> events; + + /* + * Requesting an ID > 0 will request all notifications from 0 again but filter those + * already seen notifications with ID = 1 + */ + + // This mock will also test that the NotificationFilter works as expected + Mockito.when(hmsClient.getNextNotification(Mockito.eq(0L), Mockito.eq(Integer.MAX_VALUE), + (NotificationFilter) Mockito.notNull())).thenAnswer(new Answer<NotificationEventResponse>() { + @Override + public NotificationEventResponse answer(InvocationOnMock invocation) + throws Throwable { + NotificationFilter filter = (NotificationFilter) invocation.getArguments()[2]; + NotificationEventResponse response = new NotificationEventResponse(); + + List<NotificationEvent> events = Arrays.<NotificationEvent>asList( + new NotificationEvent(1L, 0, "CREATE_DATABASE", ""), + new NotificationEvent(1L, 0, "CREATE_TABLE", ""), + new NotificationEvent(2L, 0, "ALTER_TABLE", "") + ); + + for (NotificationEvent event : events) { + String hash = UniquePathsUpdate.sha1(event); + + // We simulate that CREATE_DATABASE is already processed + if (event.getEventType().equals("CREATE_DATABASE")) { + Mockito.when(store.isNotificationProcessed(Mockito.eq(hash))).thenReturn(true); + } else { + Mockito.when(store.isNotificationProcessed(Mockito.eq(hash))).thenReturn(false); + } + + if (filter.accept(event)) { + response.addToEvents(event); + } + } + + return response; + } + }); + + events = fetcher.fetchNotifications(1); + assertEquals(2, events.size()); + assertEquals(1, events.get(0).getEventId()); + assertEquals("CREATE_TABLE", events.get(0).getEventType()); + assertEquals(2, events.get(1).getEventId()); + assertEquals("ALTER_TABLE", events.get(1).getEventType()); + } + } +} http://git-wip-us.apache.org/repos/asf/sentry/blob/9351d19d/sentry-provider/sentry-provider-db/src/test/java/org/apache/sentry/service/thrift/TestSentryHMSClient.java ---------------------------------------------------------------------- diff --git a/sentry-provider/sentry-provider-db/src/test/java/org/apache/sentry/service/thrift/TestSentryHMSClient.java b/sentry-provider/sentry-provider-db/src/test/java/org/apache/sentry/service/thrift/TestSentryHMSClient.java new file mode 100644 index 0000000..38668ca --- /dev/null +++ b/sentry-provider/sentry-provider-db/src/test/java/org/apache/sentry/service/thrift/TestSentryHMSClient.java @@ -0,0 +1,344 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.sentry.service.thrift; + +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.hive.metastore.HiveMetaStoreClient; +import org.apache.hadoop.hive.metastore.api.CurrentNotificationEventId; +import org.apache.hadoop.hive.metastore.api.Database; +import org.apache.hadoop.hive.metastore.api.MetaException; +import org.apache.hadoop.hive.metastore.api.Partition; +import org.apache.hadoop.hive.metastore.api.StorageDescriptor; +import org.apache.hadoop.hive.metastore.api.Table; +import org.apache.sentry.provider.db.service.persistent.PathsImage; +import org.apache.thrift.TException; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import javax.security.auth.login.LoginException; + +/** + * Test mocks HiveMetaStoreClient class and tests SentryHMSClient. + */ +public class TestSentryHMSClient { + + private static final Configuration conf = new Configuration(); + private static SentryHMSClient client; + private static MockHMSClientFactory hiveConnectionFactory; + + /** + * Create mock database with the given name + * + * @param name Database name + * @return Mock database object + */ + private static Database makeDb(String name) { + Database db = Mockito.mock(Database.class); + Mockito.when(db.getName()).thenReturn(name); + Mockito.when(db.getLocationUri()).thenReturn("hdfs:///" + name); + return db; + } + + /** + * Create mock table + * + * @param dbName db for this table + * @param tableName name of the table + * @return mock table object + */ + private static Table makeTable(String dbName, String tableName) { + Table table = Mockito.mock(Table.class); + Mockito.when(table.getDbName()).thenReturn(dbName); + Mockito.when(table.getTableName()).thenReturn(tableName); + StorageDescriptor sd = Mockito.mock(StorageDescriptor.class); + Mockito.when(sd.getLocation()).thenReturn( + String.format("hdfs:///%s/%s", dbName, tableName)); + Mockito.when(table.getSd()).thenReturn(sd); + return table; + } + + /** + * Create mock partition + * + * @param dbName database for this partition + * @param tableName table for this partition + * @param partName partition name + * @return mock partition object + */ + private static Partition makePartition(String dbName, String tableName, String partName) { + Partition partition = Mockito.mock(Partition.class); + StorageDescriptor sd = Mockito.mock(StorageDescriptor.class); + Mockito.when(sd.getLocation()).thenReturn( + String.format("hdfs:///%s/%s/%s", dbName, tableName, partName)); + Mockito.when(partition.getSd()).thenReturn(sd); + return partition; + } + + @BeforeClass + static public void initialize() throws IOException, LoginException { + hiveConnectionFactory = new MockHMSClientFactory(); + client = new SentryHMSClient(conf, (HiveConnectionFactory)hiveConnectionFactory); + } + + /** + * Creating snapshot when SentryHMSClient is not connected to HMS + */ + @Test + public void testSnapshotCreationWithOutClientConnected() throws Exception { + // Make sure that client is not connected + Assert.assertFalse(client.isConnected()); + PathsImage snapshotInfo = client.getFullSnapshot(); + Assert.assertTrue(snapshotInfo.getPathImage().isEmpty()); + } + + /** + * Creating snapshot when HMS doesn't have any data + */ + @Test + public void testSnapshotCreationWithNoHmsData() throws Exception { + MockClient mockClient = new MockClient(new HiveSnapshot(), 1); + client.setClient(mockClient.client); + // Make sure that client is connected + Assert.assertTrue(client.isConnected()); + PathsImage snapshotInfo = client.getFullSnapshot(); + Assert.assertTrue(snapshotInfo.getPathImage().isEmpty()); + } + + /** + * Creating a snapshot when there is data but there are updates to HMS data mean while + */ + @Test + public void testSnapshotCreationWhenDataIsActivelyUpdated() throws Exception { + HiveTable tab21 = new HiveTable("tab21"); + HiveTable tab31 = new HiveTable("tab31").add("part311").add("part312"); + HiveDb db3 = new HiveDb("db3", Lists.newArrayList(tab31)); + HiveDb db2 = new HiveDb("db2", Lists.newArrayList(tab21)); + HiveDb db1 = new HiveDb("db1"); + HiveSnapshot snap = new HiveSnapshot().add(db1).add(db2).add(db3); + final MockClient mockClient = new MockClient(snap, 1); + + client.setClient(mockClient.client); + hiveConnectionFactory.setClient(mockClient); + // Make sure that client is connected + Assert.assertTrue(client.isConnected()); + PathsImage snapshotInfo = client.getFullSnapshot(); + // Make sure that snapshot is not empty + Assert.assertTrue(!snapshotInfo.getPathImage().isEmpty()); + + Mockito.when(mockClient.client.getCurrentNotificationEventId()). + thenAnswer(new Answer<CurrentNotificationEventId>() { + @Override + public CurrentNotificationEventId answer(InvocationOnMock invocation) + throws Throwable { + return new CurrentNotificationEventId(mockClient.incrementNotificationEventId()); + } + + }); + + snapshotInfo = client.getFullSnapshot(); + Assert.assertTrue(snapshotInfo.getPathImage().isEmpty()); + } + + /** + * Creating a snapshot when there is data in HMS. + */ + @Test + public void testSnapshotCreationSuccess() throws Exception { + HiveTable tab21 = new HiveTable("tab21"); + HiveTable tab31 = new HiveTable("tab31"); + HiveDb db3 = new HiveDb("db3", Lists.newArrayList(tab31)); + HiveDb db2 = new HiveDb("db2", Lists.newArrayList(tab21)); + HiveDb db1 = new HiveDb("db1"); + HiveSnapshot snap = new HiveSnapshot().add(db1).add(db2).add(db3); + MockClient mockClient = new MockClient(snap, 1); + Mockito.when(mockClient.client.getCurrentNotificationEventId()). + thenReturn(new CurrentNotificationEventId(mockClient.eventId)); + client.setClient(mockClient.client); + hiveConnectionFactory.setClient(mockClient); + // Make sure that client is connected + Assert.assertTrue(client.isConnected()); + + PathsImage snapshotInfo = client.getFullSnapshot(); + Assert.assertEquals(5, snapshotInfo.getPathImage().size()); + Assert.assertEquals(Sets.newHashSet("db1"), snapshotInfo.getPathImage().get("db1")); + Assert.assertEquals(Sets.newHashSet("db2"), snapshotInfo.getPathImage().get("db2")); + Assert.assertEquals(Sets.newHashSet("db3"), snapshotInfo.getPathImage().get("db3")); + Assert.assertEquals(Sets.newHashSet("db2/tab21"), + snapshotInfo.getPathImage().get("db2.tab21")); + Assert.assertEquals(Sets.newHashSet("db3/tab31"), snapshotInfo.getPathImage().get("db3.tab31")); + Assert.assertEquals(snapshotInfo.getId(), mockClient.eventId); + + } + + /** + * Representation of a Hive table. A table has a name and a list of partitions. + */ + private static class HiveTable { + + private final String name; + private final List<String> partitions; + + HiveTable(String name) { + this.name = name; + this.partitions = new ArrayList<>(); + } + + HiveTable add(String partition) { + partitions.add(partition); + return this; + } + } + + /** + * Representation of a Hive database. A database has a name and a list of tables + */ + private static class HiveDb { + + final String name; + Collection<HiveTable> tables; + + @SuppressWarnings("SameParameterValue") + HiveDb(String name) { + this.name = name; + tables = new ArrayList<>(); + } + + HiveDb(String name, Collection<HiveTable> tables) { + this.name = name; + this.tables = tables; + if (this.tables == null) { + this.tables = new ArrayList<>(); + } + } + + void add(HiveTable table) { + this.tables.add(table); + } + } + + /** + * Representation of a full Hive snapshot. A snapshot is collection of databases + */ + private static class HiveSnapshot { + + final List<HiveDb> databases = new ArrayList<>(); + + HiveSnapshot() { + } + + HiveSnapshot(Collection<HiveDb> dblist) { + if (dblist != null) { + databases.addAll(dblist); + } + } + + HiveSnapshot add(HiveDb db) { + this.databases.add(db); + return this; + } + } + + /** + * Mock for HMSClientFactory + */ + private static class MockHMSClientFactory implements HiveConnectionFactory { + + private HiveMetaStoreClient mClient; + + public MockHMSClientFactory() { + mClient = null; + } + + void setClient(MockClient mockClient) { + this.mClient = mockClient.client; + } + @Override + public HMSClient connect() throws IOException, InterruptedException, MetaException { + return new HMSClient(mClient); + } + + @Override + public void close() throws Exception { + } + } + + /** + * Convert Hive snapshot to mock client that will return proper values + * for the snapshot. + */ + private static class MockClient { + + public HiveMetaStoreClient client; + public long eventId; + + MockClient(HiveSnapshot snapshot, long eventId) throws TException { + this.eventId = eventId; + client = Mockito.mock(HiveMetaStoreClient.class); + List<String> dbNames = new ArrayList<>(snapshot.databases.size()); + // Walk over all databases and mock appropriate objects + for (HiveDb mdb : snapshot.databases) { + String dbName = mdb.name; + dbNames.add(dbName); + Database db = makeDb(dbName); + Mockito.when(client.getDatabase(dbName)).thenReturn(db); + List<String> tableNames = new ArrayList<>(mdb.tables.size()); + // Walk over all tables for the database and mock appropriate objects + for (HiveTable table : mdb.tables) { + String tableName = table.name; + tableNames.add(tableName); + Table mockTable = makeTable(dbName, tableName); + Mockito.when(client.getTableObjectsByName(dbName, + Lists.newArrayList(tableName))) + .thenReturn(Lists.newArrayList(mockTable)); + Mockito.when(client.listPartitionNames(dbName, tableName, (short) -1)) + .thenReturn(table.partitions); + // Walk across all partitions and mock appropriate objects + for (String partName : table.partitions) { + Partition p = makePartition(dbName, tableName, partName); + Mockito.when(client.getPartitionsByNames(dbName, tableName, + Lists.<String>newArrayList(partName))) + .thenReturn(Lists.<Partition>newArrayList(p)); + } + } + Mockito.when(client.getAllTables(dbName)).thenReturn(tableNames); + } + // Return all database names + Mockito.when(client.getAllDatabases()).thenReturn(dbNames); + Mockito.when(client.getCurrentNotificationEventId()). + thenReturn(new CurrentNotificationEventId(eventId)); + + } + + public Long incrementNotificationEventId() { + eventId = eventId + 1; + return eventId; + } + } +} http://git-wip-us.apache.org/repos/asf/sentry/blob/9351d19d/sentry-provider/sentry-provider-db/src/test/java/org/apache/sentry/service/thrift/TestSentryStateBank.java ---------------------------------------------------------------------- diff --git a/sentry-provider/sentry-provider-db/src/test/java/org/apache/sentry/service/thrift/TestSentryStateBank.java b/sentry-provider/sentry-provider-db/src/test/java/org/apache/sentry/service/thrift/TestSentryStateBank.java new file mode 100644 index 0000000..4f71e1c --- /dev/null +++ b/sentry-provider/sentry-provider-db/src/test/java/org/apache/sentry/service/thrift/TestSentryStateBank.java @@ -0,0 +1,84 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for additional information regarding + * copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package org.apache.sentry.service.thrift; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.HashSet; +import org.junit.Before; +import org.junit.Test; + +/** + * + */ +public class TestSentryStateBank { + + @Before + public void setUp() { + SentryStateBank.clearAllStates(); + } + + @Test + public void testEnableState() { + SentryStateBank.enableState(TestState.COMPONENT, TestState.FIRST_STATE); + assertTrue("Expected FIRST_STATE to be enabled", + SentryStateBank.isEnabled(TestState.COMPONENT, TestState.FIRST_STATE)); + assertFalse("Expected SECOND_STATE to be disabled", + SentryStateBank.isEnabled(TestState.COMPONENT, TestState.SECOND_STATE)); + } + + @Test + public void testStatesGetDisabled() { + SentryStateBank.enableState(TestState.COMPONENT, TestState.FIRST_STATE); + assertTrue("Expected FIRST_STATE to be enabled", + SentryStateBank.isEnabled(TestState.COMPONENT, TestState.FIRST_STATE)); + SentryStateBank.disableState(TestState.COMPONENT, TestState.FIRST_STATE); + assertFalse("Expected FIRST_STATE to be disabled", + SentryStateBank.isEnabled(TestState.COMPONENT, TestState.FIRST_STATE)); + } + + @Test + public void testCheckMultipleStateCheckSuccess() { + SentryStateBank.enableState(TestState.COMPONENT, TestState.FIRST_STATE); + SentryStateBank.enableState(TestState.COMPONENT, TestState.SECOND_STATE); + + assertTrue("Expected both FIRST_STATE and SECOND_STATE to be enabled", + SentryStateBank.hasStatesEnabled(TestState.COMPONENT, new HashSet<SentryState>( + Arrays.asList(TestState.FIRST_STATE, TestState.SECOND_STATE)))); + } + + @Test + public void testCheckMultipleStateCheckFailure() { + SentryStateBank.enableState(TestState.COMPONENT, TestState.FIRST_STATE); + assertFalse("Expected only FIRST_STATE to be enabled", + SentryStateBank.hasStatesEnabled(TestState.COMPONENT, new HashSet<SentryState>( + Arrays.asList(TestState.FIRST_STATE, TestState.SECOND_STATE)))); + } + + + public enum TestState implements SentryState { + FIRST_STATE, + SECOND_STATE; + + public static final String COMPONENT = "TestState"; + + @Override + public long getValue() { + return 1 << this.ordinal(); + } + } +} http://git-wip-us.apache.org/repos/asf/sentry/blob/9351d19d/sentry-service/pom.xml ---------------------------------------------------------------------- diff --git a/sentry-service/pom.xml b/sentry-service/pom.xml index b63467b..0388476 100644 --- a/sentry-service/pom.xml +++ b/sentry-service/pom.xml @@ -31,8 +31,6 @@ limitations under the License. <modules> <module>sentry-service-api</module> - <module>sentry-service-server</module> - <module>sentry-service-client</module> </modules> </project> http://git-wip-us.apache.org/repos/asf/sentry/blob/9351d19d/sentry-service/sentry-service-client/pom.xml ---------------------------------------------------------------------- diff --git a/sentry-service/sentry-service-client/pom.xml b/sentry-service/sentry-service-client/pom.xml deleted file mode 100644 index a1ae8c8..0000000 --- a/sentry-service/sentry-service-client/pom.xml +++ /dev/null @@ -1,69 +0,0 @@ -<?xml version="1.0"?> -<!-- -Licensed to the Apache Software Foundation (ASF) under one or more -contributor license agreements. See the NOTICE file distributed with -this work for additional information regarding copyright ownership. -The ASF licenses this file to You under the Apache License, Version 2.0 -(the "License"); you may not use this file except in compliance with -the License. You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. ---> -<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> - <modelVersion>4.0.0</modelVersion> - <parent> - <groupId>org.apache.sentry</groupId> - <artifactId>sentry-service</artifactId> - <version>2.1.0-SNAPSHOT</version> - </parent> - - <artifactId>sentry-service-client</artifactId> - <name>Sentry Service Client</name> - - <dependencies> - <dependency> - <groupId>commons-lang</groupId> - <artifactId>commons-lang</artifactId> - </dependency> - <dependency> - <groupId>org.apache.derby</groupId> - <artifactId>derby</artifactId> - </dependency> - <dependency> - <groupId>org.slf4j</groupId> - <artifactId>slf4j-api</artifactId> - </dependency> - <dependency> - <groupId>org.slf4j</groupId> - <artifactId>slf4j-log4j12</artifactId> - </dependency> - <dependency> - <groupId>org.apache.thrift</groupId> - <artifactId>libfb303</artifactId> - <version>${libfb303.version}</version> - </dependency> - <dependency> - <groupId>org.apache.thrift</groupId> - <artifactId>libthrift</artifactId> - <version>${libthrift.version}</version> - </dependency> - <dependency> - <groupId>org.apache.sentry</groupId> - <artifactId>sentry-service-api</artifactId> - <version>${project.version}</version> - </dependency> - <dependency> - <groupId>junit</groupId> - <artifactId>junit</artifactId> - <scope>test</scope> - </dependency> - </dependencies> - -</project> \ No newline at end of file http://git-wip-us.apache.org/repos/asf/sentry/blob/9351d19d/sentry-service/sentry-service-client/src/main/java/org/apache/sentry/service/thrift/SentryServiceClientFactory.java ---------------------------------------------------------------------- diff --git a/sentry-service/sentry-service-client/src/main/java/org/apache/sentry/service/thrift/SentryServiceClientFactory.java b/sentry-service/sentry-service-client/src/main/java/org/apache/sentry/service/thrift/SentryServiceClientFactory.java deleted file mode 100644 index d146a0d..0000000 --- a/sentry-service/sentry-service-client/src/main/java/org/apache/sentry/service/thrift/SentryServiceClientFactory.java +++ /dev/null @@ -1,111 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * <p> - * http://www.apache.org/licenses/LICENSE-2.0 - * <p> - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.sentry.service.thrift; - -import org.apache.hadoop.conf.Configuration; -import org.apache.sentry.core.common.transport.RetryClientInvocationHandler; -import org.apache.sentry.core.common.transport.SentryPolicyClientTransportConfig; -import org.apache.sentry.core.common.transport.SentryTransportFactory; -import org.apache.sentry.core.common.transport.SentryTransportPool; -import org.apache.sentry.api.service.thrift.SentryPolicyServiceClient; -import org.apache.sentry.api.service.thrift.SentryPolicyServiceClientDefaultImpl; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.annotation.concurrent.ThreadSafe; -import java.lang.reflect.Proxy; -import java.util.concurrent.atomic.AtomicReference; - -/** - * Client factory for Hive clients. The factory uses connection pooling. - */ -@ThreadSafe -public final class SentryServiceClientFactory { - private static final Logger LOGGER = LoggerFactory.getLogger(SentryServiceClientFactory.class); - - private static final SentryPolicyClientTransportConfig transportConfig = - new SentryPolicyClientTransportConfig(); - private final Configuration conf; - private final SentryTransportPool transportPool; - - /** Keep track of singleton instances */ - private static final AtomicReference<SentryServiceClientFactory> clientFactory = - new AtomicReference<>(); - - /** - * Create a client instance. The supplied configuration is only used the first time and - * ignored afterwords. Tests that want to supply different configurations - * should call {@link #factoryReset(SentryServiceClientFactory)} to force new configuration - * read. - * @param conf Configuration - * @return client instance - * @throws Exception - */ - public static SentryPolicyServiceClient create(Configuration conf) throws Exception { - SentryServiceClientFactory factory = clientFactory.get(); - if (factory != null) { - return factory.create(); - } - factory = new SentryServiceClientFactory(conf); - boolean ok = clientFactory.compareAndSet(null, factory); - if (ok) { - return factory.create(); - } - // Close old factory - factory.close(); - return clientFactory.get().create(); - } - - /** - * Create a new instance of the factory which will hand hand off connections from - * the pool. - * @param conf Configuration object - */ - private SentryServiceClientFactory(Configuration conf) { - this.conf = conf; - - transportPool = new SentryTransportPool(conf, transportConfig, - new SentryTransportFactory(conf, transportConfig)); - } - - private SentryPolicyServiceClient create() throws Exception { - return (SentryPolicyServiceClient) Proxy - .newProxyInstance(SentryPolicyServiceClientDefaultImpl.class.getClassLoader(), - SentryPolicyServiceClientDefaultImpl.class.getInterfaces(), - new RetryClientInvocationHandler(conf, - new SentryPolicyServiceClientDefaultImpl(conf, transportPool), transportConfig)); - } - - /** - * Reset existing factory and return the old one. - * Only used by tests. - */ - public static SentryServiceClientFactory factoryReset(SentryServiceClientFactory factory) { - LOGGER.debug("factory reset"); - return clientFactory.getAndSet(factory); - } - - public void close() { - try { - transportPool.close(); - } catch (Exception e) { - LOGGER.error("failed to close transport pool", e); - } - } -} http://git-wip-us.apache.org/repos/asf/sentry/blob/9351d19d/sentry-service/sentry-service-server/pom.xml ---------------------------------------------------------------------- diff --git a/sentry-service/sentry-service-server/pom.xml b/sentry-service/sentry-service-server/pom.xml deleted file mode 100644 index a103c1e..0000000 --- a/sentry-service/sentry-service-server/pom.xml +++ /dev/null @@ -1,321 +0,0 @@ -<?xml version="1.0"?> -<!-- -Licensed to the Apache Software Foundation (ASF) under one or more -contributor license agreements. See the NOTICE file distributed with -this work for additional information regarding copyright ownership. -The ASF licenses this file to You under the Apache License, Version 2.0 -(the "License"); you may not use this file except in compliance with -the License. You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. ---> -<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> - <modelVersion>4.0.0</modelVersion> - <parent> - <groupId>org.apache.sentry</groupId> - <artifactId>sentry-service</artifactId> - <version>2.1.0-SNAPSHOT</version> - </parent> - - <artifactId>sentry-service-server</artifactId> - <name>Sentry Service Server</name> - - <dependencies> - <dependency> - <groupId>commons-lang</groupId> - <artifactId>commons-lang</artifactId> - </dependency> - <dependency> - <groupId>org.apache.derby</groupId> - <artifactId>derby</artifactId> - </dependency> - <dependency> - <groupId>org.slf4j</groupId> - <artifactId>slf4j-api</artifactId> - </dependency> - <dependency> - <groupId>org.slf4j</groupId> - <artifactId>slf4j-log4j12</artifactId> - </dependency> - <dependency> - <groupId>org.apache.thrift</groupId> - <artifactId>libfb303</artifactId> - <version>${libfb303.version}</version> - </dependency> - <dependency> - <groupId>org.apache.thrift</groupId> - <artifactId>libthrift</artifactId> - <version>${libthrift.version}</version> - </dependency> - <dependency> - <groupId>org.apache.curator</groupId> - <artifactId>curator-test</artifactId> - <scope>test</scope> - </dependency> - <dependency> - <groupId>org.apache.sentry</groupId> - <artifactId>sentry-service-api</artifactId> - <version>${project.version}</version> - </dependency> - <dependency> - <groupId>org.apache.sentry</groupId> - <artifactId>sentry-provider-file</artifactId> - <version>${project.version}</version> - </dependency> - <dependency> - <groupId>org.apache.sentry</groupId> - <artifactId>sentry-service-client</artifactId> - <version>${project.version}</version> - </dependency> - <dependency> - <groupId>org.apache.sentry</groupId> - <artifactId>sentry-core-common</artifactId> - <version>${project.version}</version> - </dependency> - <dependency> - <groupId>org.apache.sentry</groupId> - <artifactId>sentry-core-model-db</artifactId> - </dependency> - <dependency> - <groupId>org.apache.sentry</groupId> - <artifactId>sentry-core-model-kafka</artifactId> - </dependency> - <dependency> - <groupId>org.apache.sentry</groupId> - <artifactId>sentry-core-model-solr</artifactId> - </dependency> - <dependency> - <groupId>org.apache.sentry</groupId> - <artifactId>sentry-core-model-sqoop</artifactId> - </dependency> - <dependency> - <groupId>org.apache.sentry</groupId> - <artifactId>sentry-hdfs-common</artifactId> - </dependency> - <dependency> - <groupId>org.apache.sentry</groupId> - <artifactId>sentry-binding-hive-follower</artifactId> - <version>${project.version}</version> - <exclusions> - <exclusion> - <groupId>org.apache.hive</groupId> - <artifactId>hive-exec</artifactId> - </exclusion> - </exclusions> - </dependency> - <dependency> - <groupId>org.datanucleus</groupId> - <artifactId>datanucleus-core</artifactId> - <version>${datanucleus-core.version}</version> - </dependency> - <dependency> - <groupId>org.datanucleus</groupId> - <artifactId>datanucleus-api-jdo</artifactId> - <version>${datanucleus-api-jdo.version}</version> - </dependency> - <dependency> - <groupId>org.datanucleus</groupId> - <artifactId>datanucleus-rdbms</artifactId> - <version>${datanucleus-rdbms.version}</version> - </dependency> - <dependency> - <groupId>org.datanucleus</groupId> - <artifactId>javax.jdo</artifactId> - <version>${datanucleus-jdo.version}</version> - </dependency> - <dependency> - <groupId>org.apache.hive</groupId> - <artifactId>hive-metastore</artifactId> - <version>${hive.version}</version> - <exclusions> - <!-- This dependency needs to be excluded to avoid compilation errors in the Eclipse build. - Without this change, the Eclipse build classpath contains this jar file ahead of - datanucleus javax.jdo*.jar. This error can not be reproduced with the maven build. - Cause of compilation error : PersistenceManager class provided as part of this version, - does not implement java.lang.AutoClosable interface. This breaks the usage of - PersistenceManager inside try-with-resources clause in Sentry TransactionManager class. - --> - <exclusion> - <groupId>javax.jdo</groupId> - <artifactId>jdo-api</artifactId> - </exclusion> - <exclusion> - <groupId>javax.jdo</groupId> - <artifactId>jdo2-api</artifactId> - </exclusion> - <exclusion> - <groupId>javax.jdo</groupId> - <artifactId>jdo2-api-legacy</artifactId> - </exclusion> - </exclusions> - </dependency> - <dependency> - <groupId>org.codehaus.jackson</groupId> - <artifactId>jackson-core-asl</artifactId> - </dependency> - <dependency> - <groupId>org.codehaus.jackson</groupId> - <artifactId>jackson-mapper-asl</artifactId> - </dependency> - <dependency> - <groupId>io.dropwizard.metrics</groupId> - <artifactId>metrics-core</artifactId> - <version>${metrics.version}</version> - </dependency> - <dependency> - <groupId>io.dropwizard.metrics</groupId> - <artifactId>metrics-servlets</artifactId> - <version>${metrics.version}</version> - </dependency> - <dependency> - <groupId>io.dropwizard.metrics</groupId> - <artifactId>metrics-jvm</artifactId> - <version>${metrics.version}</version> - </dependency> - <dependency> - <groupId>org.eclipse.jetty</groupId> - <artifactId>jetty-server</artifactId> - <version>${jetty.version}</version> - </dependency> - <dependency> - <groupId>org.eclipse.jetty</groupId> - <artifactId>jetty-servlet</artifactId> - <version>${jetty.version}</version> - </dependency> - <dependency> - <groupId>org.mockito</groupId> - <artifactId>mockito-all</artifactId> - <scope>test</scope> - </dependency> - <dependency> - <groupId>org.apache.hadoop</groupId> - <artifactId>hadoop-minikdc</artifactId> - <scope>test</scope> - </dependency> - <dependency> - <groupId>junit</groupId> - <artifactId>junit</artifactId> - <scope>test</scope> - </dependency> - </dependencies> - - <build> - <sourceDirectory>${basedir}/src/main/java</sourceDirectory> - <testSourceDirectory>${basedir}/src/test/java</testSourceDirectory> - <resources> - <resource> - <directory>../../sentry-service/sentry-service-server/src/main/java/org/apache/sentry/provider/db/service/model</directory> - <includes> - <include>package.jdo</include> - </includes> - </resource> - <resource> - <directory>${basedir}/src/main</directory> - <includes> - <include>webapp/*</include> - <include>webapp/css/*</include> - </includes> - </resource> - </resources> - <plugins> - <plugin> - <groupId>com.google.code.maven-replacer-plugin</groupId> - <artifactId>replacer</artifactId> - <version>1.5.2</version> - <executions> - <execution> - <id>replaceTokens</id> - <phase>clean</phase> - <goals> - <goal>replace</goal> - </goals> - </execution> - </executions> - <configuration> - <file>${basedir}/src/main/webapp/SentryService.html</file> - <replacements> - <replacement> - <token>%PROJECT_VERSION%</token> - <value>${version}</value> - </replacement> - </replacements> - </configuration> - </plugin> - <plugin> - <groupId>org.datanucleus</groupId> - <artifactId>datanucleus-maven-plugin</artifactId> - <configuration> - <api>JDO</api> - <metadataIncludes>**/*.jdo</metadataIncludes> - <verbose>true</verbose> - </configuration> - <executions> - <execution> - <phase>process-classes</phase> - <goals> - <goal>enhance</goal> - </goals> - </execution> - </executions> - </plugin> - <plugin> - <groupId>org.apache.maven.plugins</groupId> - <artifactId>maven-shade-plugin</artifactId> - <version>${maven.shade.plugin.version}</version> - <executions> - <execution> - <id>curator-shade</id> - <phase>package</phase> - <goals> - <goal>shade</goal> - </goals> - <configuration> - <artifactSet> - <includes> - <!-- This is needed to support projects running on different versions of curator --> - <include>org.apache.curator:curator-recipes</include> - <include>org.apache.curator:curator-x-discovery</include> - <include>org.apache.curator:curator-framework</include> - <include>org.apache.curator:curator-client</include> - </includes> - </artifactSet> - <relocations> - <!-- Adding prefix to the package to make it unique --> - <relocation> - <pattern>org.apache.curator</pattern> - <shadedPattern>sentry.org.apache.curator</shadedPattern> - </relocation> - </relocations> - <shadedArtifactAttached>false</shadedArtifactAttached> - </configuration> - </execution> - </executions> - </plugin> - <plugin> - <groupId>org.apache.maven.plugins</groupId> - <artifactId>maven-jar-plugin</artifactId> - <executions> - <execution> - <goals> - <goal>test-jar</goal> - </goals> - </execution> - </executions> - </plugin> - <plugin> - <groupId>org.apache.maven.plugins</groupId> - <artifactId>maven-surefire-plugin</artifactId> - <configuration> - <reuseForks>false</reuseForks> - </configuration> - </plugin> - </plugins> - </build> -</project> \ No newline at end of file
