/* * Copyright 2015-2016 MongoDB, Inc. * * Licensed 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 com.mongodb.client; import com.mongodb.ClusterFixture; import com.mongodb.MongoClient; import com.mongodb.MongoClientOptions; import com.mongodb.MongoException; import com.mongodb.MongoNamespace; import com.mongodb.ReadPreference; import com.mongodb.client.test.CollectionHelper; import com.mongodb.connection.ServerVersion; import com.mongodb.connection.TestCommandListener; import com.mongodb.event.CommandEvent; import com.mongodb.event.CommandFailedEvent; import com.mongodb.event.CommandStartedEvent; import com.mongodb.event.CommandSucceededEvent; import org.bson.BsonArray; import org.bson.BsonBoolean; import org.bson.BsonDocument; import org.bson.BsonDocumentWriter; import org.bson.BsonDouble; import org.bson.BsonInt32; import org.bson.BsonInt64; import org.bson.BsonString; import org.bson.BsonValue; import org.bson.Document; import org.bson.codecs.BsonDocumentCodec; import org.bson.codecs.BsonValueCodecProvider; import org.bson.codecs.Codec; import org.bson.codecs.DocumentCodec; import org.bson.codecs.EncoderContext; import org.bson.codecs.configuration.CodecProvider; import org.bson.codecs.configuration.CodecRegistries; import org.bson.codecs.configuration.CodecRegistry; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import util.JsonPoweredTestHelper; import java.io.File; import java.io.IOException; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.concurrent.TimeUnit; import static com.mongodb.ClusterFixture.isDiscoverableReplicaSet; import static com.mongodb.ClusterFixture.isSharded; import static com.mongodb.ClusterFixture.serverVersionAtLeast; import static com.mongodb.Fixture.getMongoClientURI; import static java.util.Arrays.asList; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assume.assumeFalse; // See https://github.com/mongodb/specifications/tree/master/source/command-monitoring/tests @RunWith(Parameterized.class) public class CommandMonitoringTest { private static final CodecRegistry CODEC_REGISTRY_HACK = CodecRegistries.fromProviders(new BsonValueCodecProvider(), new CodecProvider() { @Override @SuppressWarnings("unchecked") public <T> Codec<T> get(final Class<T> clazz, final CodecRegistry registry) { // Use BsonDocumentCodec even for a private sub-class of BsonDocument if (BsonDocument.class.isAssignableFrom(clazz)) { return (Codec<T>) new BsonDocumentCodec(registry); } return null; } }); private static MongoClient mongoClient; private static TestCommandListener commandListener; private final String filename; private final String description; private final String databaseName; private final String collectionName; private final BsonArray data; private final BsonDocument definition; private MongoCollection<BsonDocument> collection; private JsonPoweredCrudTestHelper helper; public CommandMonitoringTest(final String filename, final String description, final String databaseName, final String collectionName, final BsonArray data, final BsonDocument definition) { this.filename = filename; this.description = description; this.databaseName = databaseName; this.collectionName = collectionName; this.data = data; this.definition = definition; } @BeforeClass public static void beforeClass() { commandListener = new TestCommandListener(); mongoClient = new MongoClient(getMongoClientURI(MongoClientOptions.builder().addCommandListener(commandListener))); } @AfterClass public static void afterClass() { if (mongoClient != null) { mongoClient.close(); } } @Before public void setUp() { ServerVersion serverVersion = ClusterFixture.getServerVersion(); if (definition.containsKey("ignore_if_server_version_less_than")) { assumeFalse(serverVersion.compareTo(getServerVersion("ignore_if_server_version_less_than")) < 0); } if (definition.containsKey("ignore_if_server_version_greater_than")) { assumeFalse(serverVersion.compareTo(getServerVersion("ignore_if_server_version_greater_than")) > 0); } if (definition.containsKey("ignore_if_topology_type")) { BsonArray topologyTypes = definition.getArray("ignore_if_topology_type"); for (BsonValue type : topologyTypes) { String typeString = type.asString().getValue(); if (typeString.equals("sharded")) { assumeFalse(isSharded()); } else if (typeString.equals("replica_set")) { assumeFalse(isDiscoverableReplicaSet()); } else if (typeString.equals("standalone")) { assumeFalse(isSharded()); } } } List<BsonDocument> documents = new ArrayList<BsonDocument>(); for (BsonValue document : data) { documents.add(document.asDocument()); } CollectionHelper<Document> collectionHelper = new CollectionHelper<Document>(new DocumentCodec(), new MongoNamespace(databaseName, collectionName)); collectionHelper.drop(); collectionHelper.insertDocuments(documents); commandListener.reset(); collection = mongoClient.getDatabase(databaseName).getCollection(collectionName, BsonDocument.class); if (definition.getDocument("operation").containsKey("read_preference")) { collection = collection.withReadPreference(ReadPreference.valueOf(definition.getDocument("operation") .getDocument("read_preference") .getString("mode").getValue())); } helper = new JsonPoweredCrudTestHelper(description, collection); } private ServerVersion getServerVersion(final String fieldName) { String[] versionStringArray = definition.getString(fieldName).getValue().split("\\."); return new ServerVersion(Integer.parseInt(versionStringArray[0]), Integer.parseInt(versionStringArray[1])); } @Test public void shouldPassAllOutcomes() { // On server <= 2.4, insertMany generates an insert command for every document, so the test fails assumeFalse(filename.startsWith("insertMany") && !serverVersionAtLeast(2, 6)); executeOperation(); List<CommandEvent> expectedEvents = getExpectedEvents(definition.getArray("expectations")); List<CommandEvent> events = commandListener.getEvents(); assertEquals(expectedEvents.size(), events.size()); for (int i = 0; i < events.size(); i++) { CommandEvent actual = events.get(i); CommandEvent expected = expectedEvents.get(i); assertEquals(expected.getClass(), actual.getClass()); assertEquals(expected.getCommandName(), actual.getCommandName()); if (actual.getClass().equals(CommandStartedEvent.class)) { CommandStartedEvent actualCommandStartedEvent = massageActualCommandStartedEvent((CommandStartedEvent) actual); CommandStartedEvent expectedCommandStartedEvent = (CommandStartedEvent) expected; assertEquals(expectedCommandStartedEvent.getDatabaseName(), actualCommandStartedEvent.getDatabaseName()); assertEquals(expectedCommandStartedEvent.getCommand(), actualCommandStartedEvent.getCommand()); } else if (actual.getClass().equals(CommandSucceededEvent.class)) { CommandSucceededEvent actualCommandSucceededEvent = massageActualCommandSucceededEvent((CommandSucceededEvent) actual); CommandSucceededEvent expectedCommandSucceededEvent = massageExpectedCommandSucceededEvent((CommandSucceededEvent) expected); assertEquals(expectedCommandSucceededEvent.getCommandName(), actualCommandSucceededEvent.getCommandName()); assertTrue(actualCommandSucceededEvent.getElapsedTime(TimeUnit.NANOSECONDS) > 0); if (expectedCommandSucceededEvent.getResponse() == null) { assertNull(actualCommandSucceededEvent.getResponse()); } else { assertTrue(String.format("\nExpected: %s\nActual: %s", expectedCommandSucceededEvent.getResponse(), actualCommandSucceededEvent.getResponse()), actualCommandSucceededEvent.getResponse().entrySet() .containsAll(expectedCommandSucceededEvent.getResponse().entrySet())); } } else if (actual.getClass().equals(CommandFailedEvent.class)) { // nothing else to assert here } else { throw new UnsupportedOperationException("Unsupported event type: " + actual.getClass()); } } } private CommandSucceededEvent massageExpectedCommandSucceededEvent(final CommandSucceededEvent expected) { // massage numbers that are the wrong BSON type expected.getResponse().put("ok", new BsonDouble(expected.getResponse().getNumber("ok").doubleValue())); return expected; } private CommandSucceededEvent massageActualCommandSucceededEvent(final CommandSucceededEvent actual) { BsonDocument response = getWritableCloneOfCommand(actual.getResponse()); // massage numbers that are the wrong BSON type response.put("ok", new BsonDouble(response.getNumber("ok").doubleValue())); if (response.containsKey("n")) { response.put("n", new BsonInt32(response.getNumber("n").intValue())); } if (actual.getCommandName().equals("find") || actual.getCommandName().equals("getMore")) { if (response.containsKey("cursor")) { if (response.getDocument("cursor").containsKey("id") && !response.getDocument("cursor").getInt64("id").equals(new BsonInt64(0))) { response.getDocument("cursor").put("id", new BsonInt64(42)); } } } else if (actual.getCommandName().equals("killCursors")) { response.getArray("cursorsUnknown").set(0, new BsonInt64(42)); } else if (isWriteCommand(actual.getCommandName())) { if (response.containsKey("writeErrors")) { for (Iterator<BsonValue> iter = response.getArray("writeErrors").iterator(); iter.hasNext();) { BsonDocument cur = iter.next().asDocument(); cur.put("code", new BsonInt32(42)); cur.put("errmsg", new BsonString("")); } } if (actual.getCommandName().equals("update")) { response.remove("nModified"); } } return new CommandSucceededEvent(actual.getRequestId(), actual.getConnectionDescription(), actual.getCommandName(), response, actual.getElapsedTime(TimeUnit.NANOSECONDS)); } private boolean isWriteCommand(final String commandName) { return asList("insert", "update", "delete").contains(commandName); } private CommandStartedEvent massageActualCommandStartedEvent(final CommandStartedEvent actual) { BsonDocument command = getWritableCloneOfCommand(actual.getCommand()); if (actual.getCommandName().equals("update")) { for (Iterator<BsonValue> iter = command.getArray("updates").iterator(); iter.hasNext();) { BsonDocument curUpdate = iter.next().asDocument(); if (!curUpdate.containsKey("multi")) { curUpdate.put("multi", BsonBoolean.FALSE); } if (!curUpdate.containsKey("upsert")) { curUpdate.put("upsert", BsonBoolean.FALSE); } } } else if (actual.getCommandName().equals("getMore")) { command.put("getMore", new BsonInt64(42)); } else if (actual.getCommandName().equals("killCursors")) { command.getArray("cursors").set(0, new BsonInt64(42)); } return new CommandStartedEvent(actual.getRequestId(), actual.getConnectionDescription(), actual.getDatabaseName(), actual.getCommandName(), command); } private void executeOperation() { try { helper.getOperationResults(definition.getDocument("operation")); } catch (MongoException e) { // ignore, as some of these are expected to throw exceptions } } private List<CommandEvent> getExpectedEvents(final BsonArray expectedEventDocuments) { List<CommandEvent> expectedEvents = new ArrayList<CommandEvent>(expectedEventDocuments.size()); for (Iterator<BsonValue> iterator = expectedEventDocuments.iterator(); iterator.hasNext();) { BsonDocument curExpectedEventDocument = iterator.next().asDocument(); String eventType = curExpectedEventDocument.keySet().iterator().next(); BsonDocument eventDescriptionDocument = curExpectedEventDocument.getDocument(eventType); CommandEvent commandEvent; if (eventType.equals("command_started_event")) { commandEvent = new CommandStartedEvent(1, null, databaseName, eventDescriptionDocument.getString("command_name").getValue(), eventDescriptionDocument.getDocument("command")); } else if (eventType.equals("command_succeeded_event")) { BsonDocument replyDocument = eventDescriptionDocument.get("reply").asDocument(); commandEvent = new CommandSucceededEvent(1, null, eventDescriptionDocument.getString("command_name").getValue(), replyDocument, 1); } else if (eventType.equals("command_failed_event")) { commandEvent = new CommandFailedEvent(1, null, eventDescriptionDocument.getString("command_name").getValue(), 1, null); } else { throw new UnsupportedOperationException("Unsupported command event type: " + eventType); } expectedEvents.add(commandEvent); } return expectedEvents; } private BsonDocument getWritableCloneOfCommand(final BsonDocument original) { BsonDocument clone = new BsonDocument(); BsonDocumentWriter writer = new BsonDocumentWriter(clone); new BsonDocumentCodec(CODEC_REGISTRY_HACK).encode(writer, original, EncoderContext.builder().build()); return clone; } @Parameterized.Parameters(name = "{1}") public static Collection<Object[]> data() throws URISyntaxException, IOException { List<Object[]> data = new ArrayList<Object[]>(); for (File file : JsonPoweredTestHelper.getTestFiles("/command-monitoring")) { BsonDocument testDocument = JsonPoweredTestHelper.getTestDocument(file); for (BsonValue test : testDocument.getArray("tests")) { data.add(new Object[]{file.getName(), test.asDocument().getString("description").getValue(), testDocument.getString("database_name").getValue(), testDocument.getString("collection_name").getValue(), testDocument.getArray("data"), test.asDocument()}); } } return data; } }