blob: 13f1592612f343e3ee810784c88725bb1fee4bc8 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2012-2014 EclipseSource Muenchen GmbH and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Julian Sommerfeldt - initial API and implementation
* Philip Langer - Bug 445193: Allow configuration of the numbers/types of changes being applied by model mutator
* Edgar Mueller - Bug 447483: ModelMutator should support cross-resource model generation and mutation, Refactorings
******************************************************************************/
package org.eclipse.emf.emfstore.modelmutator;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Random;
import java.util.Set;
import org.eclipse.emf.common.util.TreeIterator;
import org.eclipse.emf.ecore.EClass;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EReference;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.emf.emfstore.internal.modelmutator.mutation.AddObjectMutation;
import org.eclipse.emf.emfstore.internal.modelmutator.mutation.AttributeChangeMutation;
import org.eclipse.emf.emfstore.internal.modelmutator.mutation.DeleteObjectMutation;
import org.eclipse.emf.emfstore.internal.modelmutator.mutation.FeatureMapKeyMutation;
import org.eclipse.emf.emfstore.internal.modelmutator.mutation.FeatureMapValueMutation;
import org.eclipse.emf.emfstore.internal.modelmutator.mutation.MoveObjectMutation;
import org.eclipse.emf.emfstore.internal.modelmutator.mutation.Mutation;
import org.eclipse.emf.emfstore.internal.modelmutator.mutation.ReferenceChangeMutation;
import com.google.common.base.Predicate;
/**
* Abstract implementation of an model mutator.
*
* @author Julian Sommerfeldt
* @since 2.0
*
*/
public abstract class ESAbstractModelMutator {
private ESModelMutatorConfiguration modelMutatorConfig;
private final Map<EReference, List<EClass>> referencesToClasses = new LinkedHashMap<EReference, List<EClass>>();
// randomly eobjects are saved into this map and are used later instead of creating new ones
private final Map<EClass, List<EObject>> freeObjects = new LinkedHashMap<EClass, List<EObject>>();
private final Map<EClass, List<EObject>> allObjects = new LinkedHashMap<EClass, List<EObject>>();
private ESModelMutatorUtil util;
private int currentObjectCount;
private int targetObjectCount;
private int currentWidth = 1;
private int currentDepth = 1;
private List<Mutation> defaultMutationPrototypes;
/**
* Default constructor.
*
* Leaves the config uninitialized.
*/
public ESAbstractModelMutator() {
modelMutatorConfig = null;
util = null;
targetObjectCount = -1;
}
/**
* Constructor.
*
* @param config
* The {@link ESModelMutatorConfiguration} to use for mutation.
*/
public ESAbstractModelMutator(ESModelMutatorConfiguration config) {
modelMutatorConfig = config;
util = new ESModelMutatorUtil(config);
targetObjectCount = config.getMinObjectsCount();
}
/**
* Called before generation and mutation.
*/
public abstract void preMutate();
/**
* Called after generation and mutation.
*/
public abstract void postMutate();
/**
* Initial generation.
*/
public void generate() {
preMutate();
// generate till there are enough objects
while (currentObjectCount < targetObjectCount) {
createChildrenForRoot();
currentWidth++;
if (randomBoolean() && randomBoolean() && randomBoolean()) {
currentDepth++;
}
}
postMutate();
}
/**
* Mutation after an initial generation.
*
* @param ignoredFeatures
* a set of features to be ignored while mutating
*/
public void mutate(Set<EStructuralFeature> ignoredFeatures) {
if (modelMutatorConfig.getMutationCount() == -1) {
performFullMutation(ignoredFeatures);
} else {
performConfiguredNumberOfMutations();
}
}
/**
* Performs mutation until the given {@link Predicate} is met.
* The predicate input will be the the root object obtained by
* calling {@code getConfig()}.
*
* @param predicate
* the predicate that must be fulfilled in order to stop mutation
*/
public void mutateUntil(final Predicate<EObject> predicate) {
mutate(Collections.<EStructuralFeature> emptySet());
while (!predicate.apply(getRootEObject())) {
mutate(Collections.<EStructuralFeature> emptySet());
}
}
private void performFullMutation(Set<EStructuralFeature> ignoredFeatures) {
final EObject rootEObject = getRootEObject();
deleteEObjects(rootEObject);
currentObjectCount = ESModelMutatorUtil.getAllObjectsCount(rootEObject);
generate();
changeCrossReferences();
mutateAttributes(ignoredFeatures);
}
/**
* Returns the root {@link EObject} of the {@link ESModelMutatorConfiguration}.
* Clients may override.
*
* @return the root {@link EObject} of the {@link ESModelMutatorConfiguration}
*/
protected EObject getRootEObject() {
return modelMutatorConfig.getRootEObject();
}
private void performConfiguredNumberOfMutations() {
final List<Mutation> mutations = getDefaultMutationPrototypes();
int i = 0;
while (i < modelMutatorConfig.getMutationCount()) {
final int rndIdx = modelMutatorConfig.getRandom().nextInt(mutations.size());
final Mutation nextMutation = mutations.get(rndIdx);
final Mutation mutationToRun = nextMutation.clone();
try {
mutationToRun.apply();
i++;
} catch (final ESMutationException ex) {
continue;
}
}
}
private List<Mutation> getDefaultMutationPrototypes() {
if (defaultMutationPrototypes == null) {
defaultMutationPrototypes = createDefaultMutationPrototypes();
}
return defaultMutationPrototypes;
}
private List<Mutation> createDefaultMutationPrototypes() {
final List<Mutation> defaultMutationPrototypes = new ArrayList<Mutation>();
defaultMutationPrototypes.add(new AddObjectMutation(util));
defaultMutationPrototypes.add(new DeleteObjectMutation(util));
defaultMutationPrototypes.add(new MoveObjectMutation(util));
defaultMutationPrototypes.add(new AttributeChangeMutation(util));
defaultMutationPrototypes.add(new ReferenceChangeMutation(util));
defaultMutationPrototypes.add(new FeatureMapKeyMutation(util));
defaultMutationPrototypes.add(new FeatureMapValueMutation(util));
return defaultMutationPrototypes;
}
/**
* Create the children for the root object.
*/
public void createChildrenForRoot() {
final EObject rootEObject = getRootEObject();
// if the root depth should not be generated
if (modelMutatorConfig.isDoNotGenerateRoot()) {
boolean didCreateChild = false;
// create children for each of the children of the root
final List<EObject> eContents = new ArrayList<EObject>(rootEObject.eContents());
try {
for (final EObject obj : eContents) {
didCreateChild |= createChildren(obj, 1);
}
} catch (final IndexOutOfBoundsException e) {
e.printStackTrace();
}
if (!didCreateChild) {
createChildren(rootEObject, 0);
}
} else {
// if the root depth should be generated, create children for the root
createChildren(rootEObject, 0);
}
}
/**
* Create recursively direct and indirect children for a given {@link EObject} and its children.
*
* @param root The {@link EObject} for which children should be generated.
* @param depth The depth of the EObject in the total tree.
*
* @return {@code true}, if children have been created, {@code false} otherwise
*/
public boolean createChildren(EObject root, int depth) {
if (currentObjectCount >= targetObjectCount) {
return false;
}
// check if we reached the maximal depth
if (depth >= currentDepth) {
return false;
}
// create children for the current root object
final List<EObject> children = createChildren(root);
// create children for the children (one step deeper)
for (final EObject obj : children) {
createChildren(obj, depth + 1);
}
return !children.isEmpty();
}
/**
* Creates/deletes direct children for the given {@link EObject}.
*
* @param root The {@link EObject} for which children should be created.
* @return A list of the newly generated children
*/
public List<EObject> createChildren(EObject root) {
final List<EObject> children = new ArrayList<EObject>();
final Collection<EStructuralFeature> ignore = modelMutatorConfig.geteStructuralFeaturesToIgnore();
// iterate over all references
for (final EReference reference : root.eClass().getEAllContainments()) {
// check if the reference is valid: not to be ignored AND can be set/added
if (ignore.contains(reference) || !util.isValid(reference, root)) {
continue;
}
// add remaining children (specified through modelMutatorConfig)
final int init = currentWidth / 2 - root.eContents().size();
// add children to fulfill width constraint
for (int i = init; i > 0; i--) {
final EClass eClass = getValidEClass(reference);
if (eClass != null) {
final EObject obj = getEObject(eClass, new LinkedHashSet<EStructuralFeature>(ignore));
// randomly first changeCrossReferences
if (randomBoolean()) {
changeCrossReferences(obj);
}
// add directly to parent or add to free objects
// only add if it is many or if it is only one
if (reference.isMany() || i == 1) {
addToParent(root, obj, reference);
children.add(obj);
currentObjectCount++;
currentObjectCount += obj.eContents().size();
} else {
addToEClassToObjectsMap(obj, freeObjects);
}
addToEClassToObjectsMap(obj, allObjects);
if (currentObjectCount >= targetObjectCount) {
return children;
}
}
}
}
return children;
}
private boolean randomBoolean() {
return modelMutatorConfig.getRandom().nextBoolean();
}
/**
* Randomly deletes direct and indirect children of the given root {@link EObject}.
*
* @param root The {@link EObject} from which children should be deleted.
*/
public void deleteEObjects(EObject root) {
final List<EObject> toDelete = new ArrayList<EObject>();
final Random random = modelMutatorConfig.getRandom();
final int maxDeleteCount = modelMutatorConfig.getMaxDeleteCount();
// randomly select objects to delete
int deleted = 0;
for (final TreeIterator<EObject> it = root.eAllContents(); it.hasNext();) {
final EObject obj = it.next();
if (deleted < maxDeleteCount && randomBoolean()) {
toDelete.add(obj);
deleted++;
addToEClassToObjectsMap(obj, freeObjects);
}
addToEClassToObjectsMap(obj, allObjects);
}
// check for delete modes to use
final List<Integer> deleteModes = new ArrayList<Integer>();
deleteModes.add(ESModelMutatorUtil.DELETE_DELETE_COMMAND);
deleteModes.add(ESModelMutatorUtil.DELETE_CUT_CONTAINMENT);
if (modelMutatorConfig.isUseEcoreUtilDelete()) {
deleteModes.add(ESModelMutatorUtil.DELETE_ECORE);
}
// delete selected objects
final int size = deleteModes.size();
for (final EObject obj : new ArrayList<EObject>(toDelete)) {
util.removeFullPerCommand(obj, deleteModes.get(random.nextInt(size)));
}
}
private void addToEClassToObjectsMap(EObject obj, Map<EClass, List<EObject>> map) {
List<EObject> objects = map.get(obj.eClass());
if (objects == null) {
objects = new ArrayList<EObject>();
map.put(obj.eClass(), objects);
}
objects.add(obj);
}
/**
* Get a {@link EClass}, which is valid for the given {@link EReference}.
*
* @param reference The {@link EReference} to search a {@link EClass} for.
* @return A valid {@link EClass} for the given {@link EReference} or <code>null</code> if there is none.
*/
protected EClass getValidEClass(EReference reference) {
// get already saved classes list
List<EClass> classes = referencesToClasses.get(reference);
// if there is none, create it
if (classes == null) {
// get all classes of the modelpackage
classes = util.getAllEContainments(reference);
// check if they should be ignored
for (final EClass eClass : modelMutatorConfig.geteClassesToIgnore()) {
classes.remove(eClass);
classes.removeAll(util.getAllSubEClasses(eClass));
}
// remove classes which cannot have an instance
for (final EClass eClass : new ArrayList<EClass>(classes)) {
if (!ESModelMutatorUtil.canHaveInstance(eClass)) {
classes.remove(eClass);
}
}
// return null if there is no valid class
if (classes.isEmpty()) {
return null;
}
// save for future use
referencesToClasses.put(reference, classes);
}
// randomly select one class
final int index = modelMutatorConfig.getRandom().nextInt(classes.size());
return classes.get(index);
}
/**
* Creates a new {@link EObject} and sets its attributes.
*
* @param eClass The {@link EClass} of the new {@link EObject}.
* @param ignoredFeatures
* a set of features to be ignored while mutating
* @return The newly created and modified {@link EObject}.
*/
protected EObject getEObject(EClass eClass, Set<EStructuralFeature> ignoredFeatures) {
final Random random = modelMutatorConfig.getRandom();
EObject newObject = null;
// try to get an already existing object if there is one
final List<EObject> objects = freeObjects.get(eClass);
if (objects != null && objects.size() != 0 && randomBoolean()) {
newObject = objects.remove(random.nextInt(objects.size()));
} else {
newObject = EcoreUtil.create(eClass);
}
util.setEObjectAttributes(newObject, ignoredFeatures);
return newObject;
}
/**
* Adds an {@link EObject} to the given parent.
*
* @param parent The {@link EObject} where to add the newObject
* @param newObject The new {@link EObject} to add to the parent.
* @param reference The {@link EReference} where to add the newObject.
*/
protected void addToParent(EObject parent, EObject newObject, EReference reference) {
if (reference.isMany()) {
util.addPerCommand(parent, reference, newObject, randomBoolean() ? 0 : null);
} else {
util.setPerCommand(parent, reference, newObject);
}
}
/**
* Randomly mutates all attributes.
*
* @param ignoredFeatures
* a set of features to be ignored while mutating
*/
public void mutateAttributes(Set<EStructuralFeature> ignoredFeatures) {
final EObject rootEObject = getRootEObject();
for (final TreeIterator<EObject> it = rootEObject.eAllContents(); it.hasNext();) {
final EObject obj = it.next();
util.setEObjectAttributes(obj, ignoredFeatures);
}
}
/**
* Changes cross references of all {@link EObject}s in the model.
*/
public void changeCrossReferences() {
for (final Entry<EClass, List<EObject>> entry : allObjects.entrySet()) {
for (final EObject obj : entry.getValue()) {
changeCrossReferences(obj);
}
}
}
/**
* Changes CrossReferences of an specific {@link EObject}.
*
* @param obj The {@link EObject} whose cross references should be changed
*/
public void changeCrossReferences(EObject obj) {
for (final EReference reference : util.getValidCrossReferences(obj)) {
for (final EClass referenceClass : util.getReferenceClasses(reference, allObjects.keySet())) {
util.setReference(obj, referenceClass, reference, allObjects);
}
}
}
/**
* @return the modelMutatorConfig
*/
public ESModelMutatorConfiguration getConfig() {
return modelMutatorConfig;
}
/**
* Sets the config to be used by the mutator.
*
* @param config
* the config to be used by the mutator
*/
public void setConfig(ESModelMutatorConfiguration config) {
modelMutatorConfig = config;
util = new ESModelMutatorUtil(config);
targetObjectCount = config.getMinObjectsCount();
}
}