/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates.
*
* 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 org.optaplanner.core.impl.score.director.drools.testgen;
import java.io.File;
import java.util.ArrayDeque;
import java.util.Collection;
import java.util.Deque;
import java.util.List;
import org.kie.api.runtime.KieSession;
import org.optaplanner.core.api.score.Score;
import org.optaplanner.core.api.score.constraint.ConstraintMatchTotal;
import org.optaplanner.core.api.score.holder.ScoreHolder;
import org.optaplanner.core.impl.domain.entity.descriptor.EntityDescriptor;
import org.optaplanner.core.impl.domain.variable.descriptor.VariableDescriptor;
import org.optaplanner.core.impl.score.definition.ScoreDefinition;
import org.optaplanner.core.impl.score.director.drools.DroolsScoreDirector;
import org.optaplanner.core.impl.score.director.drools.DroolsScoreDirectorFactory;
import org.optaplanner.core.impl.score.director.drools.testgen.reproducer.TestGenCorruptedScoreException;
import org.optaplanner.core.impl.score.director.drools.testgen.reproducer.TestGenCorruptedScoreReproducer;
import org.optaplanner.core.impl.score.director.drools.testgen.reproducer.TestGenCorruptedVariableListenerReproducer;
import org.optaplanner.core.impl.score.director.drools.testgen.reproducer.TestGenDroolsExceptionReproducer;
public class TestGenDroolsScoreDirector<Solution_> extends DroolsScoreDirector<Solution_> {
private static final String TEST_CLASS_NAME = "DroolsReproducerTest";
private final TestGenKieSessionJournal journal = new TestGenKieSessionJournal();
private final File testFile = new File(TEST_CLASS_NAME + ".java");
private final TestGenTestWriter writer = new TestGenTestWriter();
private final Deque<String> oldValues = new ArrayDeque<>();
public TestGenDroolsScoreDirector(
DroolsScoreDirectorFactory<Solution_> scoreDirectorFactory,
boolean lookUpEnabled,
boolean constraintMatchEnabledPreference,
List<String> scoreDrlList,
List<File> scoreDrlFileList) {
super(scoreDirectorFactory, lookUpEnabled, constraintMatchEnabledPreference);
writer.setClassName(TEST_CLASS_NAME);
writer.setScoreDefinition(scoreDirectorFactory.getScoreDefinition());
writer.setConstraintMatchEnabled(constraintMatchEnabledPreference);
writer.setScoreDrlList(scoreDrlList);
writer.setScoreDrlFileList(scoreDrlFileList);
}
public KieSession createKieSession() {
KieSession newKieSession = getScoreDirectorFactory().newKieSession();
// set a fresh score holder
ScoreDefinition<?> scoreDefinition = getScoreDefinition();
if (scoreDefinition != null) {
ScoreHolder sh = scoreDefinition.buildScoreHolder(constraintMatchEnabledPreference);
newKieSession.setGlobal(DroolsScoreDirector.GLOBAL_SCORE_HOLDER_KEY, sh);
}
return newKieSession;
}
@Override
public void setWorkingSolution(Solution_ workingSolution) {
super.setWorkingSolution(workingSolution);
journal.dispose();
Collection<Object> workingFacts = getWorkingFacts();
journal.addFacts(workingFacts);
for (Object fact : workingFacts) {
journal.insertInitial(fact);
}
}
@Override
public Score calculateScore() {
journal.fireAllRules();
try {
return super.calculateScore();
} catch (RuntimeException e) {
// catch any Drools exception and create a minimal reproducing test
// TODO check the exception is coming from org.drools
TestGenDroolsExceptionReproducer reproducer = new TestGenDroolsExceptionReproducer(e, this);
TestGenKieSessionJournal minJournal = TestGenerator.minimize(journal, reproducer);
writer.print(minJournal, testFile);
throw wrapOriginalException(e);
}
}
@Override
public void assertShadowVariablesAreNotStale(Score expectedWorkingScore, Object completedAction) {
try {
journal.enterAssertMode();
super.assertShadowVariablesAreNotStale(expectedWorkingScore, completedAction);
journal.exitAssertMode();
} catch (IllegalStateException e) {
// catch corrupted VariableListener exception and create a minimal reproducing test
if (e.getMessage().startsWith("Impossible")) {
TestGenCorruptedVariableListenerReproducer reproducer
= new TestGenCorruptedVariableListenerReproducer(e.getMessage(), this);
// FIXME this is currently broken. The pruning needs to be smarter and not remove genuine variable
// updates that directly affect shadow variables in the last (corrupted) variable listeners update.
// If the genuine update is removed the shadow update obviously becomes inconsistent, which leads
// to a false positive and the journal no longer reproduces the original issue. This is the current
// state.
TestGenKieSessionJournal minJournal = TestGenerator.minimize(journal, reproducer);
try {
minJournal.replay(createKieSession());
throw new IllegalStateException();
} catch (TestGenCorruptedScoreException tgcse) {
writer.setCorruptedScoreException(tgcse);
}
writer.print(minJournal, testFile);
throw wrapOriginalException(e);
} else {
throw new UnsupportedOperationException("Stale shadow variable reproducer not implemented.");
}
}
}
@Override
public void assertWorkingScoreFromScratch(Score workingScore, Object completedAction) {
try {
super.assertWorkingScoreFromScratch(workingScore, completedAction);
} catch (IllegalStateException e) {
// catch corrupted score exception and create a minimal reproducing test
// TODO check it's really corrupted score
TestGenCorruptedScoreReproducer reproducer = new TestGenCorruptedScoreReproducer(e.getMessage(), this);
TestGenKieSessionJournal minJournal = TestGenerator.minimize(journal, reproducer);
try {
minJournal.replay(createKieSession());
throw new IllegalStateException();
} catch (TestGenCorruptedScoreException tgcse) {
writer.setCorruptedScoreException(tgcse);
}
writer.print(minJournal, testFile);
throw wrapOriginalException(e);
}
}
@Override
public Collection<ConstraintMatchTotal> getConstraintMatchTotals() {
journal.fireAllRules();
return super.getConstraintMatchTotals();
}
@Override
public void dispose() {
journal.dispose();
super.dispose();
}
@Override
public void afterEntityAdded(EntityDescriptor<Solution_> entityDescriptor, Object entity) {
journal.insert(entity);
super.afterEntityAdded(entityDescriptor, entity);
}
@Override
public void beforeVariableChanged(VariableDescriptor variableDescriptor, Object entity) {
if (logger.isTraceEnabled()) {
Object oldValue = variableDescriptor.getValue(entity);
if (oldValue == null) {
// ArrayDeque doesn't allow null values
oldValues.push("null");
} else {
oldValues.push(oldValue.toString());
}
}
super.beforeVariableChanged(variableDescriptor, entity);
}
@Override
public void afterVariableChanged(VariableDescriptor variableDescriptor, Object entity) {
super.afterVariableChanged(variableDescriptor, entity);
journal.update(entity, variableDescriptor);
if (logger.isTraceEnabled()) {
logger.trace(" Updating variable {}.{}[{}]: {} → {}",
entity,
variableDescriptor.getVariableName(),
variableDescriptor.getVariablePropertyType().getSimpleName(),
oldValues.pop(),
variableDescriptor.getValue(entity));
}
}
@Override
public void afterEntityRemoved(EntityDescriptor<Solution_> entityDescriptor, Object entity) {
journal.delete(entity);
super.afterEntityRemoved(entityDescriptor, entity);
}
@Override
public void afterProblemFactAdded(Object problemFact) {
journal.insert(problemFact);
super.afterProblemFactAdded(problemFact);
}
// TODO override afterProblemFactChanged()?
@Override
public void afterProblemFactRemoved(Object problemFact) {
journal.delete(problemFact);
super.afterProblemFactRemoved(problemFact);
}
private RuntimeException wrapOriginalException(RuntimeException e) {
return new RuntimeException(e.getMessage() + "\nDrools test written to: " + testFile.getAbsolutePath(), e);
}
}