/*
* Copyright 2010 JBoss 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 org.drools.testframework;
import static org.mvel2.MVEL.eval;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.drools.ClockType;
import org.drools.FactHandle;
import org.drools.RuleBase;
import org.drools.SessionConfiguration;
import org.drools.base.ClassTypeResolver;
import org.drools.base.TypeResolver;
import org.drools.common.InternalRuleBase;
import org.drools.common.InternalWorkingMemory;
import org.drools.ide.common.client.modeldriven.testing.ActivateRuleFlowGroup;
import org.drools.ide.common.client.modeldriven.testing.CallFieldValue;
import org.drools.ide.common.client.modeldriven.testing.CallMethod;
import org.drools.ide.common.client.modeldriven.testing.ExecutionTrace;
import org.drools.ide.common.client.modeldriven.testing.Expectation;
import org.drools.ide.common.client.modeldriven.testing.FactData;
import org.drools.ide.common.client.modeldriven.testing.FieldData;
import org.drools.ide.common.client.modeldriven.testing.Fixture;
import org.drools.ide.common.client.modeldriven.testing.RetractFact;
import org.drools.ide.common.client.modeldriven.testing.Scenario;
import org.drools.ide.common.client.modeldriven.testing.VerifyFact;
import org.drools.ide.common.client.modeldriven.testing.VerifyField;
import org.drools.ide.common.client.modeldriven.testing.VerifyRuleFired;
import org.drools.ide.common.server.util.ScenarioXMLPersistence;
import org.drools.rule.Package;
import org.drools.spi.Activation;
import org.drools.time.impl.PseudoClockScheduler;
import org.mvel2.MVEL;
/**
* This actually runs the test scenarios.
*/
public class ScenarioRunner {
private final Scenario scenario;
private final Map<String, Object> populatedData = new HashMap<String, Object>();
private final Map<String, Object> globalData = new HashMap<String, Object>();
private final Map<String, FactHandle> factHandles = new HashMap<String, FactHandle>();
private final InternalWorkingMemory workingMemory;
private final TypeResolver resolver;
/**
* This constructor is normally used by Guvnor for running tests on a users
* request.
*
* @param scenario
* The scenario to run.
* @param resolver
* A populated type resolved to be used to resolve the types in
* the scenario.
* <p/>
* For info on how to invoke this, see
* ContentPackageAssemblerTest.testPackageWithRuleflow in
* guvnor-webapp This requires that the classloader for the
* thread context be set appropriately. The PackageBuilder can
* provide a suitable TypeResolver for a given package header,
* and the Package config can provide a classloader.
*/
public ScenarioRunner(final Scenario scenario,
final TypeResolver resolver,
final InternalWorkingMemory wm) throws ClassNotFoundException {
this.scenario = scenario;
this.workingMemory = wm;
this.resolver = resolver;
runScenario();
}
/**
* Use this constructor if you have a scenario in a file, for instance.
*
* @throws ClassNotFoundException
*/
public ScenarioRunner(String xml,
RuleBase ruleBase) throws ClassNotFoundException {
this.scenario = ScenarioXMLPersistence.getInstance().unmarshal( xml );
SessionConfiguration sessionConfiguration = new SessionConfiguration();
sessionConfiguration.setClockType( ClockType.PSEUDO_CLOCK );
this.workingMemory = (InternalWorkingMemory) ruleBase.newStatefulSession( sessionConfiguration,
null );
ClassLoader classLoader = ((InternalRuleBase) ruleBase).getRootClassLoader();
HashSet<String> imports = getImports( ruleBase.getPackages()[0] );
this.resolver = new ClassTypeResolver( imports,
classLoader );
runScenario();
}
public HashSet<String> getImports(Package aPackage) {
HashSet<String> imports = new HashSet<String>();
imports.add( aPackage.getName() + ".*" );
imports.addAll( aPackage.getImports().keySet() );
return imports;
}
interface Populate {
public void go();
}
private void runScenario() throws ClassNotFoundException {
MVEL.COMPILER_OPT_ALLOW_NAKED_METH_CALL = true;
scenario.setLastRunResult( new Date() );
//stub out any rules we don't want to have the consequences firing of.
HashSet<String> ruleList = new HashSet<String>();
ruleList.addAll( scenario.getRules() );
TestingEventListener listener = null;
List<Populate> toPopulate = new ArrayList<Populate>();
for ( final FactData fact : scenario.getGlobals() ) {
final Object factObject = eval( "new " + getTypeName( resolver,
fact ) + "()" );
toPopulate.add( new Populate() {
public void go() {
populateFields( fact,
factObject );
}
} );
globalData.put( fact.getName(),
factObject );
this.workingMemory.setGlobal( fact.getName(),
factObject );
}
doPopulate( toPopulate );
for ( Iterator<Fixture> iterator = scenario.getFixtures().iterator(); iterator.hasNext(); ) {
Fixture fixture = iterator.next();
if ( fixture instanceof FactData ) {
//deal with facts and globals
final FactData fact = (FactData) fixture;
final Object factObject = (fact.isModify()) ? this.populatedData.get( fact.getName() ) : eval( "new " + getTypeName( resolver,
fact ) + "()" );
if ( fact.isModify() ) {
if ( !this.factHandles.containsKey( fact.getName() ) ) {
throw new IllegalArgumentException( "Was not a previously inserted fact. [" + fact.getName() + "]" );
}
toPopulate.add( new Populate() {
public void go() {
populateFields( fact,
factObject );
workingMemory.update( factHandles.get( fact.getName() ),
factObject );
}
} );
} else /* a new one */{
populatedData.put( fact.getName(),
factObject );
toPopulate.add( new Populate() {
public void go() {
populateFields( fact,
factObject );
factHandles.put( fact.getName(),
workingMemory.insert( factObject ) );
}
} );
}
} else if ( fixture instanceof RetractFact ) {
RetractFact retractFact = (RetractFact) fixture;
this.workingMemory.retract( this.factHandles.get( retractFact.getName() ) );
this.populatedData.remove( retractFact.getName() );
} else if ( fixture instanceof CallMethod ) {
CallMethod aCall = (CallMethod) (fixture);
Object targetInstance = populatedData.get( aCall.getVariable() );
executeMethodOnObject( aCall,
targetInstance );
} else if ( fixture instanceof ActivateRuleFlowGroup ) {
String ruleFlowGroupName = ((ActivateRuleFlowGroup) fixture).getName();
workingMemory.getAgenda().getRuleFlowGroup( ruleFlowGroupName ).setAutoDeactivate( false );
workingMemory.getAgenda().activateRuleFlowGroup( ruleFlowGroupName );
} else if ( fixture instanceof ExecutionTrace ) {
doPopulate( toPopulate );
ExecutionTrace executionTrace = (ExecutionTrace) fixture;
//create the listener to trace rules
if ( listener != null ) this.workingMemory.removeEventListener( listener ); //remove the old
listener = new TestingEventListener();
this.workingMemory.addEventListener( listener );
//set up the time machine
applyTimeMachine( this.workingMemory,
executionTrace );
//love you
long time = System.currentTimeMillis();
this.workingMemory.fireAllRules( listener.getAgendaFilter( ruleList,
scenario.isInclusive() ),
scenario.getMaxRuleFirings() );
executionTrace.setExecutionTimeResult( System.currentTimeMillis() - time );
executionTrace.setNumberOfRulesFired( listener.totalFires );
executionTrace.setRulesFired( listener.getRulesFiredSummary() );
} else if ( fixture instanceof Expectation ) {
doPopulate( toPopulate );
Expectation assertion = (Expectation) fixture;
if ( assertion instanceof VerifyFact ) {
verify( (VerifyFact) assertion );
} else if ( assertion instanceof VerifyRuleFired ) {
verify( (VerifyRuleFired) assertion,
(listener.firingCounts != null) ? listener.firingCounts : new HashMap<String, Integer>() );
}
} else {
throw new IllegalArgumentException( "Not sure what to do with " + fixture );
}
}
doPopulate( toPopulate );
}
private void doPopulate(List<Populate> toPopulate) {
for ( Populate p : toPopulate ) {
p.go();
}
toPopulate.clear();
}
private String getTypeName(TypeResolver resolver,
FactData fact) throws ClassNotFoundException {
String fullName = resolver.getFullTypeName( fact.getType() );
if ( fullName.equals( "java.util.List" ) || fullName.equals( "java.util.Collection" ) ) {
return "java.util.ArrayList";
} else {
return fullName;
}
}
private void applyTimeMachine(final InternalWorkingMemory wm,
ExecutionTrace executionTrace) {
long targetTime = 0;
if ( executionTrace.getScenarioSimulatedDate() != null ) {
targetTime = executionTrace.getScenarioSimulatedDate().getTime();
} else {
targetTime = new Date().getTime();
}
long currentTime = wm.getSessionClock().getCurrentTime();
((PseudoClockScheduler) wm.getSessionClock()).advanceTime( targetTime - currentTime,
TimeUnit.MILLISECONDS );
}
void verify(VerifyRuleFired assertion,
Map<String, Integer> firingCounts) {
assertion.setActualResult( firingCounts.containsKey( assertion.getRuleName() ) ? firingCounts.get( assertion.getRuleName() ) : 0 );
if ( assertion.getExpectedFire() != null ) {
if ( assertion.getExpectedFire() ) {
if ( assertion.getActualResult() > 0 ) {
assertion.setSuccessResult( true );
assertion.setExplanation( "Rule [" + assertion.getRuleName() + "] was actived " + assertion.getActualResult() + " times." );
} else {
assertion.setSuccessResult( false );
assertion.setExplanation( "Rule [" + assertion.getRuleName() + "] was not activated. Expected it to be activated." );
}
} else {
if ( assertion.getActualResult() == 0 ) {
assertion.setSuccessResult( true );
assertion.setExplanation( "Rule [" + assertion.getRuleName() + "] was not activated." );
} else {
assertion.setSuccessResult( false );
assertion.setExplanation( "Rule [" + assertion.getRuleName() + "] was activated " + assertion.getActualResult() + " times, but expected none." );
}
}
}
if ( assertion.getExpectedCount() != null ) {
if ( assertion.getActualResult().equals( assertion.getExpectedCount() ) ) {
assertion.setSuccessResult( true );
assertion.setExplanation( "Rule [" + assertion.getRuleName() + "] activated " + assertion.getActualResult() + " times." );
} else {
assertion.setSuccessResult( false );
assertion.setExplanation( "Rule [" + assertion.getRuleName() + "] activated " + assertion.getActualResult() + " times. Expected " + assertion.getExpectedCount() + " times." );
}
}
}
void verify(VerifyFact value) {
if ( !value.anonymous ) {
Object factObject = this.populatedData.get( value.getName() );
if ( factObject == null ) factObject = this.globalData.get( value.getName() );
FactFieldValueVerifier fieldVerifier = new FactFieldValueVerifier( populatedData,
value.getName(),
factObject,
resolver );
fieldVerifier.checkFields( value.getFieldValues() );
} else {
Iterator obs = this.workingMemory.iterateObjects();
while ( obs.hasNext() ) {
Object factObject = obs.next();
if ( factObject.getClass().getSimpleName().equals( value.getName() ) ) {
FactFieldValueVerifier fieldVerifier = new FactFieldValueVerifier( populatedData,
value.getName(),
factObject,
resolver );
fieldVerifier.checkFields( value.getFieldValues() );
if ( value.wasSuccessful() ) return;
}
}
for ( VerifyField vfl : value.getFieldValues() ) {
if ( vfl.getSuccessResult() == null ) {
vfl.setSuccessResult( Boolean.FALSE );
vfl.setActualResult( "No match" );
}
}
}
}
Object populateFields(FactData fact,
Object factObject) {
for ( int i = 0; i < fact.getFieldData().size(); i++ ) {
FieldData field = (FieldData) fact.getFieldData().get( i );
Object val = null;
if ( field.getValue() != null ) {
if ( field.getValue().startsWith( "=" ) ) {
// eval the val into existence
val = eval( field.getValue().substring( 1 ),
populatedData );
} else if ( field.getNature() == FieldData.TYPE_ENUM ) {
// The string representation of a java enum value is a
// format like CheeseType.CHEDDAR
String valueOfEnum = field.getValue();
String fullName = null;
if ( field.getValue().indexOf( "." ) != -1 ) {
String classNameOfEnum = field.getValue().substring( 0,
field.getValue().lastIndexOf( "." ) );
valueOfEnum = field.getValue().substring( field.getValue().lastIndexOf( "." ) + 1 );
try {
//This is a Java enum type if the type can be resolved by ClassTypeResolver
//Revisit: Better way to determine java enum type or Guvnor enum type.
fullName = resolver.getFullTypeName( classNameOfEnum );
if ( fullName != null && !"".equals( fullName ) ) {
valueOfEnum = fullName + "." + valueOfEnum;
}
val = eval( valueOfEnum );
} catch ( ClassNotFoundException e ) {
// This is a Guvnor enum type
fullName = classNameOfEnum;
if ( fullName != null && !"".equals( fullName ) ) {
valueOfEnum = fullName + "." + valueOfEnum;
}
val = valueOfEnum;
}
} else {
val = valueOfEnum;
}
} else {
val = field.getValue();
}
Map<String, Object> vars = new HashMap<String, Object>();
vars.putAll( populatedData );
vars.put( "__val__",
val );
vars.put( "__fact__",
factObject );
eval( "__fact__." + field.getName() + " = __val__",
vars );
}
}
return factObject;
}
Object executeMethodOnObject(CallMethod fact,
Object factObject) {
Map<String, Object> vars = new HashMap<String, Object>();
vars.put( "__fact__",
factObject );
String methodName = "__fact__." + fact.getMethodName() + "(";
for ( int i = 0; i < fact.getCallFieldValues().length; i++ ) {
CallFieldValue field = (CallFieldValue) fact.getCallFieldValues()[i];
Object val;
if ( field.value != null && !field.value.equals( "" ) ) {
if ( field.value.startsWith( "=" ) ) {
// eval the val into existence
val = populatedData.get( field.value.substring( 1 ) );
} else {
val = field.value;
}
vars.put( "__val" + i + "__",
val );
methodName = methodName + "__val" + i + "__";
if ( i < fact.getCallFieldValues().length - 1 ) {
methodName = methodName + ",";
}
}
}
methodName = methodName + ")";
eval( methodName,
vars );
return factObject;
}
/**
* True if the scenario was run with 100% success.
*/
public boolean wasSuccess() {
return this.scenario.wasSuccessful();
}
Scenario getScenario() {
return scenario;
}
Map<String, Object> getPopulatedData() {
return populatedData;
}
Map<String, Object> getGlobalData() {
return globalData;
}
Map<String, FactHandle> getFactHandles() {
return factHandles;
}
InternalWorkingMemory getWorkingMemory() {
return workingMemory;
}
TypeResolver getResolver() {
return resolver;
}
}