001    package org.kuali.rice.krms.framework;
002    
003    import static junit.framework.Assert.assertEquals;
004    import static junit.framework.Assert.assertTrue;
005    import static junit.framework.Assert.fail;
006    
007    import java.util.Arrays;
008    import java.util.Collections;
009    import java.util.HashMap;
010    import java.util.HashSet;
011    import java.util.LinkedList;
012    import java.util.List;
013    import java.util.Map;
014    import java.util.Set;
015    
016    import org.apache.commons.lang.StringUtils;
017    import org.junit.Before;
018    import org.junit.Test;
019    import org.kuali.rice.krms.api.engine.Term;
020    import org.kuali.rice.krms.api.engine.TermResolutionEngine;
021    import org.kuali.rice.krms.api.engine.TermResolutionException;
022    import org.kuali.rice.krms.api.engine.TermResolver;
023    import org.kuali.rice.krms.api.engine.TermSpecification;
024    import org.kuali.rice.krms.framework.engine.TermResolutionEngineImpl;
025    import org.springframework.util.CollectionUtils;
026    
027    
028    public class TermResolutionEngineTest {
029            
030            private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(TermResolutionEngineTest.class);
031    
032            private TermResolutionEngine termResolutionEngine = null;
033            
034            @Before
035            public void setUp() {
036                    termResolutionEngine = new TermResolutionEngineImpl();
037            }
038            
039            @Test
040            public void testNoResolution() {
041                    TestScenarioHelper testHelper = new TestScenarioHelper(termResolutionEngine);
042                    
043                    // GIVENS:
044                    testHelper.addGivens("A");
045    
046                    testHelper.logScenarioDescription();
047                    
048                    testHelper.assertSuccess("A");
049            }
050            
051            @Test
052            public void testSimpleResolution() {
053                    TestScenarioHelper testHelper = new TestScenarioHelper(termResolutionEngine);
054                    
055                    // GIVENS:
056                    testHelper.addGivens("A");
057                    
058                    // RESOLVERS:
059                    testHelper.addResolver("B", /* <-- */ "A");
060                    
061                    testHelper.logScenarioDescription();
062                    
063                    testHelper.assertSuccess("B");
064            }
065            
066            @Test
067            public void testTwoStepResolution() {
068                    TestScenarioHelper testHelper = new TestScenarioHelper(termResolutionEngine);
069                    
070                    // GIVENS:
071                    testHelper.addGivens("A");
072                    
073                    // RESOLVERS:
074                    testHelper.addResolver("B", /* <-- */ "A");
075                    testHelper.addResolver("C", /* <-- */ "B");
076                    
077                    testHelper.logScenarioDescription();
078                    
079                    testHelper.assertSuccess("C");
080            }
081    
082            @Test
083            public void testForkingResolution() {
084                    TestScenarioHelper testHelper = new TestScenarioHelper(termResolutionEngine);
085    
086                    // GIVENS:
087                    testHelper.addGivens("A", "Z");
088                    
089                    // RESOLVERS:
090                    testHelper.addResolver("D", /* <-- */ "B","C");
091                    testHelper.addResolver("C", /* <-- */ "Z");
092                    testHelper.addResolver("B", /* <-- */ "A");
093                    
094                    testHelper.logScenarioDescription();
095                    
096                    testHelper.assertSuccess("D");
097            }
098            
099            @Test
100            public void testMultipleValidPaths() {
101                    TestScenarioHelper testHelper = new TestScenarioHelper(termResolutionEngine);
102                    
103                    // GIVENS:
104                    testHelper.addGivens("A", "Z");
105                    
106                    // RESOLVERS:
107                    testHelper.addResolver("D", /* <-- */ "B","C");
108                    testHelper.addResolver("C", /* <-- */ "Z");
109                    testHelper.addResolver("B", /* <-- */ "A");
110                    testHelper.addResolver("D", /* <-- */ "A");
111                    
112                    testHelper.logScenarioDescription();
113                    
114                    testHelper.assertSuccess("D");
115            }
116            
117            @Test
118            public void testDiamond() {
119                    TestScenarioHelper testHelper = new TestScenarioHelper(termResolutionEngine);
120    
121                    // GIVENS:
122                    testHelper.addGivens("A");
123                    
124                    // RESOLVERS:
125                    testHelper.addResolver("D", /* <-- */ "B","C");
126                    testHelper.addResolver("C", /* <-- */ "A");
127                    testHelper.addResolver("B", /* <-- */ "A");
128                    
129                    testHelper.logScenarioDescription();
130                    
131                    testHelper.assertSuccess("D");
132                    
133            }
134    
135            @Test
136            public void testComplexPath() {
137                    TestScenarioHelper testHelper = new TestScenarioHelper(termResolutionEngine);
138    
139                    // GIVENS:
140                    testHelper.addGivens("Q","R","S");
141                    
142                    // RESOLVERS:
143                    testHelper.addResolver("A", /* <-- */ "B","F");
144                    testHelper.addResolver("A", /* <-- */ "Z");
145                    testHelper.addResolver("B", /* <-- */ "D");
146                    testHelper.addResolver("B", /* <-- */ "C");
147                    testHelper.addResolver("C", /* <-- */ "S");
148                    testHelper.addResolver("D", /* <-- */ "E");
149                    testHelper.addResolver("E", /* <-- */ "S");
150                    testHelper.addResolver("F", /* <-- */ "G","Q");
151                    testHelper.addResolver("G", /* <-- */ "R");
152                    
153                    testHelper.logScenarioDescription();
154                    
155                    testHelper.assertSuccess("A");
156                    
157            }
158    
159            @Test
160            public void testCycle() {
161                    TestScenarioHelper testHelper = new TestScenarioHelper(termResolutionEngine);
162    
163                    // GIVENS:
164                    // none
165                    
166                    // RESOLVERS:
167                    testHelper.addResolver("D", /* <-- */ "C");
168                    testHelper.addResolver("C", /* <-- */ "D");
169                    
170                    testHelper.logScenarioDescription();
171                    
172                    testHelper.assertException("D");
173                    
174            }
175            
176    
177            @Test
178            public void testUnreachableTerm() {
179                    TestScenarioHelper testHelper = new TestScenarioHelper(termResolutionEngine);
180    
181                    // GIVENS:
182                    // none
183                    
184                    // RESOLVERS:
185                    testHelper.addResolver("D", /* <-- */ "C");
186                    testHelper.addResolver("C", /* <-- */ "B");
187                    
188                    testHelper.logScenarioDescription();
189                    
190                    testHelper.assertException("D");
191                    
192            }
193            
194            @Test
195            public void testRedHerringPath() {
196                    TestScenarioHelper testHelper = new TestScenarioHelper(termResolutionEngine);
197    
198                    // GIVENS:
199                    testHelper.addGivens("Q");
200                    
201                    // RESOLVERS:
202                    testHelper.addResolver("A", /* <-- */ "B");
203                    // this is the shortest path, but C can't be resolved
204                    testHelper.addResolver("B", /* <-- */ "C");
205                    
206                    testHelper.addResolver("B", /* <-- */ "D");
207                    testHelper.addResolver("D", /* <-- */ "Q");
208                    
209                    testHelper.logScenarioDescription();
210                    
211                    testHelper.assertSuccess("A");
212            }
213            
214            @Test
215            public void testResolveParamaterizedTerm() {
216                    TestScenarioHelper testHelper = new TestScenarioHelper(termResolutionEngine);
217    
218                    // GIVENS:
219                    testHelper.addGivens("Q");
220                    
221                    // RESOLVERS:
222                    testHelper.addResolver(1, "A", new String[] {"param"}, /* <-- */ "B");
223                    testHelper.addResolver("B", /* <-- */ "Q");
224                    
225                    testHelper.logScenarioDescription();
226                    
227                    Map<String,String> params = new HashMap();
228                    params.put("param", "value");
229                    testHelper.assertSuccess(testHelper.getTerm("A", params));
230    
231                    // term A needs parameters, so this shouldn't work
232                    testHelper.assertException(new Term(testHelper.getTermSpec("A"), null));
233    
234            }
235            
236            @Test
237            public void testIntermediateParamaterizedTerm() {
238                    TestScenarioHelper testHelper = new TestScenarioHelper(termResolutionEngine);
239    
240                    // GIVENS:
241                    testHelper.addGivens("Q");
242                    
243                    // RESOLVERS:
244                    testHelper.addResolver(1, "A", new String[] {"foo"}, /* <-- */ "B");
245                    testHelper.addResolver(1, "B", new String[] {"bar"},  /* <-- */ "Q");
246                    
247                    testHelper.logScenarioDescription();
248                    
249                    Map<String,String> params = new HashMap();
250                    params.put("foo", "foovalue");
251    
252                    // Resolver for term b requires parameters, so this shouldn't work
253                    testHelper.assertException(testHelper.getTerm("A", params));
254            }
255            
256            private static class WhiteBoxTermResolutionEngineImpl extends TermResolutionEngineImpl {
257                    
258                    // expose this for testing purposes
259                    @Override
260                    public List<TermResolverKey> buildTermResolutionPlan(TermSpecification term) {
261                            return super.buildTermResolutionPlan(term);
262                    }
263            }
264            
265            
266            @Test
267            public void testShortestPath() {
268                    
269                    WhiteBoxTermResolutionEngineImpl whiteBoxTermResolutionService = new WhiteBoxTermResolutionEngineImpl();
270                    TestScenarioHelper testHelper = new TestScenarioHelper(whiteBoxTermResolutionService);
271    
272                    // GIVENS:
273                    testHelper.addGivens("Q");
274                    
275                    // RESOLVERS:
276                    // this one costs 3 (instead of default cost of 1)
277                    testHelper.addResolver(3, "A", /* <-- */ "Q");
278    
279                    // the steps for the alternate path each cost 1, but total length is 4
280                    testHelper.addResolver("A", /* <-- */ "B");
281                    testHelper.addResolver("B", /* <-- */ "C");
282                    testHelper.addResolver("C", /* <-- */ "D");
283                    testHelper.addResolver("D", /* <-- */ "Q");
284                    
285                    testHelper.logScenarioDescription();
286                    
287                    List<?> plan = whiteBoxTermResolutionService.buildTermResolutionPlan(testHelper.getTermSpec("A"));
288                    LOG.info("resolutionPlan: " + StringUtils.join(plan, ", ") + " <-- should be length 1!");
289                    assertTrue("didn't choose the shortest resolution path (of length 1)", plan.size() == 1);
290            }
291            
292            /*
293             *  TODO: test exception variants:
294             *  - TermResolver throws TermResolutionException
295             *  - TermResolutionEngine is passed a null term
296             */
297            
298            // TODO: what should the TermResolutionEngine do if a resolver throws a RuntimeException?
299            /*
300            @Test
301            public void testExplodingResolver() {
302                    TestScenarioHelper testHelper = new TestScenarioHelper(termResolutionEngine);
303    
304                    // GIVENS:
305                    testHelper.addGivens("Q");
306                    
307                    // RESOLVERS:
308                    TermResolverMock exploder = new TermResolverMock(testHelper.getTerm("A"), testHelper.getResult("A"), testHelper.getTerm("Q"));
309                    exploder.setIsExploder(true);
310                    termResolutionEngine.addTermResolver(exploder);
311                    
312                    testHelper.logScenarioDescription();
313                    
314                    testHelper.assertException("A");
315            }
316            */
317            
318            
319            private static class TermResolverMock<T> implements TermResolver<T> {
320                    private TermSpecification output;
321                    private Set<String> params;
322                    private T result;
323                    private Set<TermSpecification> prereqs;
324                    private int cost;
325                    private boolean isExploder = false;
326    
327                    public TermResolverMock(TermSpecification output, T result, int cost, TermSpecification ... prereqs) {
328                            this(output, null, result, cost, prereqs);
329                    }
330                    
331                    public TermResolverMock(TermSpecification output, Set<String> params, T result, int cost, TermSpecification ... prereqs) {
332                            this.output = output;
333                            this.params = Collections.unmodifiableSet(params);
334                            this.result = result;
335                            this.prereqs = new HashSet<TermSpecification>(Arrays.asList(prereqs));
336                            this.cost = cost;
337                    }
338                    
339                    @Override
340                    public int getCost() {
341                            return cost;
342                    }
343                    
344                    @Override
345                    public TermSpecification getOutput() {
346                            return output;
347                    }
348                    
349                    @Override
350                    public Set<TermSpecification> getPrerequisites() {
351                            return prereqs;
352                    }
353                    
354                    @Override
355                    public Set<String> getParameterNames() {
356                            return params;
357                    }
358                    
359                    public void setIsExploder(boolean isExploder) {
360                            this.isExploder = isExploder;
361                    }
362                    
363                    @Override
364                    public T resolve(Map<TermSpecification, Object> resolvedPrereqs, Map<String, String> parameters) {
365                            
366                            if (isExploder) {
367                                    throw new RuntimeException("I'm the exploder, coo coo catchoo");
368                            }
369                            
370                            // get all prereqs first
371                            for (TermSpecification prereq : prereqs) {
372                                    Object result = resolvedPrereqs.get(prereq);
373                                    if (result == null) fail("got back null for prereq " + prereq);
374                            }
375    
376                            LOG.info("resolving " + output);
377                            return result;
378                    }
379                    
380                    @Override
381                    public String toString() {
382                            String paramStr = "";
383                            if (!CollectionUtils.isEmpty(params)) {
384                                    paramStr = "+(" + StringUtils.join(params, ",") + ")";
385                            }
386                            return getClass().getSimpleName()+"[ "+output+paramStr+ " <- " + StringUtils.join(prereqs.iterator(), ",") + " ]";
387                    }
388    
389            }
390            
391    
392            private static class TestScenarioHelper {
393                    
394                    private final TermResolutionEngine ars;
395                    
396                    private List<String> givens = new LinkedList<String>();
397                    private List<String> resolvers = new LinkedList<String>();
398    
399                    public TestScenarioHelper(TermResolutionEngine tre) {
400                            this.ars = tre;
401                    }
402                    
403                    public void addGivens(String ... names) {
404                            for (String name : names) {
405                                    TermSpecification given = getTermSpec(name); // empty String type for less clutter
406                                    ars.addTermValue(new Term(given, null), getResult(name));
407                                    givens.add(name);
408                            }
409                    }
410                    
411                    public String getResult(String name) {
412                            return getResult(new Term(getTermSpec(name), null));
413                    }
414                    
415                    public String getResult(Term term) {
416                            return term.getSpecification().getName()+"-result";
417                    }
418                    
419                    public TermSpecification getTermSpec(String name) {
420                            return new TermSpecification(name, "");
421                    }
422                    
423                    public Term getTerm(String name, Map<String,String> params) {
424                            return new Term(new TermSpecification(name, ""), params);
425                    }
426                    
427                    public void addResolver(String out, String ... prereqs) {
428                            addResolver(1, out, prereqs);
429                    }
430    
431                    public void addResolver(int cost, String out, String ... prereqs) {
432                            addResolver(1, out, null, prereqs);
433                    }
434                    
435                    public void addResolver(int cost, String out, String[] params, String ... prereqs) {
436                            TermSpecification [] prereqTerms = new TermSpecification [prereqs.length];
437                            
438                            for (int i=0; i<prereqs.length; i++) prereqTerms[i] = getTermSpec(prereqs[i]);
439                            
440                            Set<String> paramSet = Collections.emptySet();
441                            if (params != null) paramSet = new HashSet<String>(Arrays.asList(params));
442                            
443                            ars.addTermResolver(new TermResolverMock<Object>(getTermSpec(out), paramSet, getResult(out), cost, prereqTerms));
444                            resolvers.add("(" + out + " <- " + StringUtils.join(prereqs, ",") + ")");
445                    }
446                    
447                    public void logScenarioDescription() {
448                            StringBuilder sb = new StringBuilder();
449                            
450                            sb.append("givens: " + StringUtils.join(givens.iterator(), ", ") + "\n\n");
451                            sb.append("resolvers:\n----------------------\n");
452                            if (resolvers == null || resolvers.size() == 0) {
453                                    sb.append("none");
454                            } else { 
455                                    sb.append(StringUtils.join(resolvers.iterator(), "\n"));
456                            }
457                            sb.append("\n");
458    
459                            LOG.info("Test Scenario:\n\n" + sb.toString());
460                    }
461                    
462                    public void assertSuccess(String toResolve) {
463                            assertSuccess(new Term(getTermSpec(toResolve), null));
464                    }
465                    
466                    public void assertSuccess(Term toResolve) {
467                            LOG.info("Testing resolution of " + toResolve);
468                            try {
469                                    assertEquals(getResult(toResolve), ars.resolveTerm(toResolve));
470                                    LOG.info("Success!");
471                            } catch (TermResolutionException e) {
472                                    fail("Should resolve the term w/o exceptions");
473                            }
474                    }
475    
476                    public void assertException(String toResolve) {
477                            assertException(new Term(getTermSpec(toResolve), null));
478                    }
479                            
480                    public void assertException(Term toResolve) {
481                            LOG.info("Testing resolution of " + toResolve);
482                            try {
483                                    ars.resolveTerm(toResolve);
484                                    fail("Should throw TermResolutionException");
485                            } catch (TermResolutionException e) {
486                                    LOG.info("Success! threw " + e);
487                                    // Good, this is what we expect
488                            }
489                    }
490            }
491            
492    }