/*
* ToroDB
* Copyright © 2014 8Kdata Technology (www.8kdata.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.torodb.mongodb.repl.oplogreplier.analyzed;
import static com.eightkdata.mongowp.bson.utils.DefaultBsonValues.*;
import static org.junit.Assert.*;
import com.eightkdata.mongowp.Status;
import com.eightkdata.mongowp.bson.BsonDocument;
import com.eightkdata.mongowp.server.api.oplog.*;
import com.torodb.kvdocument.conversion.mongowp.MongoWpConverter;
import com.torodb.kvdocument.values.KvDocument;
import com.torodb.kvdocument.values.KvInteger;
import com.torodb.kvdocument.values.KvValue;
import com.torodb.kvdocument.values.heap.StringKvString;
import com.torodb.mongodb.repl.oplogreplier.OpTimeFactory;
import org.apache.logging.log4j.Logger;
import org.jooq.lambda.function.Consumer3;
import org.junit.Test;
import java.util.Random;
import java.util.function.BiConsumer;
/**
*
*/
public abstract class AbstractAnalyzedOpTest<E extends AnalyzedOp> {
private static final Random RANDOM = new Random();
private final KvValue<?> defaultMongoDocId = KvInteger.of(1);
private static final OpTimeFactory opTimeFactory = new OpTimeFactory();
abstract E getAnalyzedOp(KvValue<?> mongoDocId);
abstract Logger getLogger();
abstract void andThenInsertTest();
abstract void andThenUpdateModTest();
abstract void andThenUpdateSetTest();
abstract void andThenUpsertModTest();
abstract void andThenUpsertSetTest();
abstract void andThenDeleteTest();
public E getDefaultOp() {
return getAnalyzedOp(defaultMongoDocId);
}
@Test
public void getMismatchErrorMessageTest() {
AnalyzedOp op = getAnalyzedOp(defaultMongoDocId);
if (op.getType().requiresMatch()) {
Status<?> status = op.getMismatchErrorMessage();
assertNotNull(
"The mismatch error of an analyzed operation that requires match must not be null", status);
assertFalse("The mismatch error of an analyzed operation that requires match must not be OK",
status.isOk());
} else {
try {
op.getMismatchErrorMessage();
fail("An analyzed operation that does not require match must throw a "
+ UnsupportedOperationException.class + " when its mismatch error is "
+ "requested");
} catch (UnsupportedOperationException ex) {
}
}
}
@Test
public void getMongoDocIdTest() {
assertEquals("Unexpected mongo doc id.", defaultMongoDocId, getAnalyzedOp(defaultMongoDocId)
.getMongoDocId());
}
@Test
public void requiresToFetchToroIdTest() {
AnalyzedOp op = getAnalyzedOp(defaultMongoDocId);
assertEquals("", op.getType().requiresToFetchToroId(), op.requiresToFetchToroId());
}
@Test
public void requiresMatchTest() {
AnalyzedOp op = getAnalyzedOp(defaultMongoDocId);
assertEquals("", op.getType().requiresMatch(), op.requiresMatch());
}
@Test
public void requiresFetchTest() {
AnalyzedOp op = getAnalyzedOp(defaultMongoDocId);
assertEquals("", op.getType().requiresFetch(), op.requiresFetch());
}
@Test
public void deletesTest() {
AnalyzedOp op = getAnalyzedOp(defaultMongoDocId);
assertEquals("", op.getType().deletes(), op.deletes());
}
//Functions to be called by the subclasses
protected <Op extends CollectionOplogOperation> void emptyConsumer3(Op colOp, KvDocument newDoc,
KvDocument matchDoc) {
getLogger().warn("An empty check was done. It should be improved");
}
protected <Op extends CollectionOplogOperation> void emptyBiConsumer(Op colOp, KvDocument newDoc) {
getLogger().warn("An empty check was done. It should be improved");
}
protected void andThenInsert(
Consumer3<InsertOplogOperation, KvDocument, KvDocument> onMatchCallback,
BiConsumer<InsertOplogOperation, KvDocument> onMismatchCallback) {
AnalyzedOp op = getDefaultOp();
AnalyzedOpType nextExpectedType = op.getType();
InsertOplogOperation colOp = new InsertOplogOperation(
(BsonDocument) MongoWpConverter.translate(createDefaultKVDoc(defaultMongoDocId)),
"db",
"col",
opTimeFactory.newOpTime(),
RANDOM.nextLong(),
OplogVersion.V1,
false
);
AnalyzedOp newOp = op.andThenInsert(colOp);
andThenGeneric(colOp, newOp, nextExpectedType, onMatchCallback, onMismatchCallback);
}
protected void andThenUpdateMod(
Consumer3<UpdateOplogOperation, KvDocument, KvDocument> onMatchCallback,
BiConsumer<UpdateOplogOperation, KvDocument> onMismatchCallback) {
AnalyzedOp op = getDefaultOp();
AnalyzedOpType nextExpectedType = op.getType();
UpdateOplogOperation colOp = createRandomUpdateModOplogOperation(defaultMongoDocId, false);
AnalyzedOp newOp = op.andThenUpdateMod(colOp);
andThenGeneric(colOp, newOp, nextExpectedType, onMatchCallback, onMismatchCallback);
}
protected void andThenUpdateSet(
Consumer3<UpdateOplogOperation, KvDocument, KvDocument> onMatchCallback,
BiConsumer<UpdateOplogOperation, KvDocument> onMismatchCallback) {
AnalyzedOp op = getDefaultOp();
AnalyzedOpType nextExpectedType = op.getType();
UpdateOplogOperation colOp = createRandomUpdateSetOplogOperation(defaultMongoDocId, false);
AnalyzedOp newOp = op.andThenUpdateMod(colOp);
andThenGeneric(colOp, newOp, nextExpectedType, onMatchCallback, onMismatchCallback);
}
protected void andThenUpsertMod(
Consumer3<UpdateOplogOperation, KvDocument, KvDocument> onMatchCallback,
BiConsumer<UpdateOplogOperation, KvDocument> onMismatchCallback) {
AnalyzedOp op = getDefaultOp();
AnalyzedOpType nextExpectedType = op.getType();
UpdateOplogOperation colOp = createRandomUpdateModOplogOperation(defaultMongoDocId, true);
AnalyzedOp newOp = op.andThenUpsertMod(colOp);
andThenGeneric(colOp, newOp, nextExpectedType, onMatchCallback, onMismatchCallback);
}
protected void andThenUpsertSet(
Consumer3<UpdateOplogOperation, KvDocument, KvDocument> onMatchCallback,
BiConsumer<UpdateOplogOperation, KvDocument> onMismatchCallback) {
AnalyzedOp op = getDefaultOp();
AnalyzedOpType nextExpectedType = op.getType();
UpdateOplogOperation colOp = createRandomUpdateSetOplogOperation(defaultMongoDocId, true);
AnalyzedOp newOp = op.andThenUpsertSet(colOp);
andThenGeneric(colOp, newOp, nextExpectedType, onMatchCallback, onMismatchCallback);
}
protected void andThenDelete(
Consumer3<DeleteOplogOperation, KvDocument, KvDocument> onMatchCallback,
BiConsumer<DeleteOplogOperation, KvDocument> onMismatchCallback) {
AnalyzedOp op = getDefaultOp();
AnalyzedOpType nextExpectedType = op.getType();
DeleteOplogOperation colOp = createRandomDeleteOplogOperation(defaultMongoDocId);
AnalyzedOp newOp = op.andThenDelete(colOp);
andThenGeneric(colOp, newOp, nextExpectedType, onMatchCallback, onMismatchCallback);
}
public KvDocument createDefaultKVDoc(KvValue<?> mongoDocId) {
return new KvDocument.Builder()
.putValue("_id", mongoDocId)
.putValue("intAtt", KvInteger.of(RANDOM.nextInt()))
.putValue("stringAtt", new StringKvString("a string value"))
.build();
}
//Functions to be called by this class utility methods
private <Op extends CollectionOplogOperation> void andThenGeneric(
Op colOp, AnalyzedOp newOp, AnalyzedOpType nextExpectedType,
Consumer3<Op, KvDocument, KvDocument> onMatchCallback,
BiConsumer<Op, KvDocument> onMismatchCallback) {
KvDocument matchDoc = createDefaultKVDoc(defaultMongoDocId);
assertEquals("Unexpected type on the resulting operation after applying a a change",
nextExpectedType, newOp.getType());
testCalculateDocToInsert(matchDoc, newOp);
onMatchCallback.accept(colOp, newOp.calculateDocToInsert(ignored -> matchDoc), matchDoc);
onMismatchCallback.accept(colOp, newOp.calculateDocToInsert((ignored -> null)));
}
private UpdateOplogOperation createRandomUpdateModOplogOperation(KvValue<?> mongoDocId,
boolean upsert) {
return new UpdateOplogOperation(
newDocument("_id", MongoWpConverter.translate(mongoDocId)),
"db",
"col",
opTimeFactory.newOpTime(),
RANDOM.nextLong(),
OplogVersion.V1,
false,
newDocument("$set", newDocument("modifiedAtt", newInt(RANDOM.nextInt()))),
upsert
);
}
private UpdateOplogOperation createRandomUpdateSetOplogOperation(KvValue<?> mongoDocId,
boolean upsert) {
return new UpdateOplogOperation(
newDocument("_id", MongoWpConverter.translate(mongoDocId)),
"db",
"col",
opTimeFactory.newOpTime(),
RANDOM.nextLong(),
OplogVersion.V1,
false,
newDocument("modifiedAtt", newInt(RANDOM.nextInt())),
upsert
);
}
private DeleteOplogOperation createRandomDeleteOplogOperation(KvValue<?> mongoDocId) {
return new DeleteOplogOperation(
newDocument("_id", MongoWpConverter.translate(mongoDocId)),
"db",
"col",
opTimeFactory.newOpTime(),
RANDOM.nextLong(),
OplogVersion.V1,
false,
false
);
}
private void testCalculateDocToInsert(KvDocument matchDoc, AnalyzedOp newOp) {
try {
KvDocument docToInsert = newOp.calculateDocToInsert(otherOp -> matchDoc);
//ops that requires fetch must return nonnull
if (newOp.getType().requiresFetch()) {
assertNotNull("The document to insert must not be null when the operation requires "
+ "to fetch ", docToInsert);
}
switch (newOp.getType()) {
case DELETE_CREATE:
case UPDATE_MOD:
case UPDATE_SET:
case UPSERT_MOD:
assertNotNull("The document to insert must not when the operation type is "
+ newOp.getType() + " and the doc was fetch", docToInsert);
break;
default:
}
} catch (NullPointerException | IllegalArgumentException ex) {
throw new AssertionError("Unexpected exception on a operation that does not "
+ "requires to fetch", ex);
}
//ops that does not require fetch must be ready to recive a function that returns null
if (!newOp.getType().requiresFetch()) {
try {
KvDocument docToInsert = newOp.calculateDocToInsert(otherOp -> null);
switch (newOp.getType()) {
case DELETE_CREATE:
case UPSERT_MOD:
assertNotNull("The document to insert must not when the operation type is "
+ newOp.getType() + " even when the doc is not fetch", docToInsert);
break;
default:
}
} catch (NullPointerException | IllegalArgumentException ex) {
throw new AssertionError("Unexpected exception on a operation that does not "
+ "requires to fetch", ex);
}
}
}
}