001 /*
002 Copyright (c) 1996-2012, Damon Hart-Davis
003 All rights reserved.
004
005 Redistribution and use in source and binary forms, with or without
006 modification, are permitted provided that the following conditions are
007 met:
008
009 * Redistributions of source code must retain the above copyright
010 notice, this list of conditions and the following disclaimer.
011
012 * Redistributions in binary form must reproduce the above copyright
013 notice, this list of conditions and the following disclaimer in the
014 documentation and/or other materials provided with the
015 distribution.
016
017 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
018 IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
019 TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
020 PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
021 OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
022 SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
023 LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
024 DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
025 THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
026 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
027 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
028 */
029
030 package org.hd.d.pg2k.test.dev;
031
032 import java.io.File;
033 import java.util.ArrayList;
034 import java.util.Collection;
035 import java.util.Collections;
036 import java.util.HashSet;
037 import java.util.Iterator;
038 import java.util.List;
039 import java.util.Map;
040 import java.util.concurrent.ConcurrentHashMap;
041 import java.util.concurrent.ConcurrentMap;
042 import java.util.concurrent.atomic.AtomicInteger;
043
044 import junit.framework.TestCase;
045
046 import org.hd.d.pg2k.svrCore.AllExhibitProperties;
047 import org.hd.d.pg2k.svrCore.FileTools;
048 import org.hd.d.pg2k.svrCore.GenUtils;
049 import org.hd.d.pg2k.svrCore.Name;
050 import org.hd.d.pg2k.svrCore.Name.ExhibitFull;
051 import org.hd.d.pg2k.webSvr.catalogue.PaginationBeanBase;
052 import org.hd.d.pg2k.webSvr.catalogue.PaginationBeanNumeric;
053 import org.hd.d.pg2k.webSvr.catalogue.PaginationBeanTree;
054 import org.hd.d.pg2k.webSvr.exhibit.TreeFilterBean;
055 import org.hd.d.pg2k.webSvr.util.WebConsts;
056
057 /**
058 * Created by IntelliJ IDEA.
059 * User: Damon Hart-Davis
060 * Date: 03-Jun-2003
061 * Time: 17:55:04
062 */
063
064 /**Tests the variants and derivatives of PaginationBeanNumeric.
065 * These beans can be used stand-alone (outside a JSP page) at least in part,
066 * so we can test the core algorithms.
067 */
068 public final class PaginationBeanTest extends TestCase
069 {
070 /**Performs test of the base bean.
071 * This ensures that the basic underlying algorithms are correct.
072 */
073 public void testPaginationBeanBase()
074 {
075 // First just create an instance of the bean.
076 final PaginationBeanNumeric pbn = new PaginationBeanNumeric();
077
078 // Test initial values.
079 assertTrue("default number of items should be zero", pbn.getNumberOfItems() == 0);
080 assertTrue("default page number should be 1", pbn.getPg() == 1);
081 // Do the generic tests with current setup.
082 testPBContract(pbn);
083
084 // Check that any attmept to set the page with zero items to paginate is quetly ignored.
085 pbn.setPg(-1);
086 assertTrue("page number should be 1", pbn.getPg() == 1);
087 pbn.setPg(9);
088 assertTrue("page number should be 1", pbn.getPg() == 1);
089 pbn.setPg("rubbish");
090 assertTrue("page number should be 1", pbn.getPg() == 1);
091
092 // Check number-of-pages algorithm.
093 pbn.setNumberOfItems(0);
094 assertTrue("0 items needs zero pages", pbn.getNumberOfPages() == 0);
095 pbn.setNumberOfItems(1);
096 assertTrue("1 item needs one page", pbn.getNumberOfPages() == 1);
097 pbn.setNumberOfItems(WebConsts.MAX_RESULTS_PER_PAGE - 1);
098 assertTrue("1 item less than the max per page needs one page", pbn.getNumberOfPages() == 1);
099 pbn.setNumberOfItems(WebConsts.MAX_RESULTS_PER_PAGE);
100 assertTrue("the max per page needs one page", pbn.getNumberOfPages() == 1);
101 pbn.setNumberOfItems(WebConsts.MAX_RESULTS_PER_PAGE + 1);
102 assertTrue("1 item more than the max per page needs two pages", pbn.getNumberOfPages() == 2);
103 pbn.setNumberOfItems(99 * WebConsts.MAX_RESULTS_PER_PAGE + 1);
104 assertTrue("wrong result for many items", pbn.getNumberOfPages() == 100);
105 // Do the generic tests with current setup.
106 testPBContract(pbn);
107
108 // Test the (plain-old-int) page-setting algorithm.
109 pbn.setNumberOfItems(10 * WebConsts.MAX_RESULTS_PER_PAGE);
110 assertTrue("wrong result for ten-pages' worth of items", pbn.getNumberOfPages() == 10);
111 pbn.setPg(3);
112 assertTrue("did not set page number correctly to 3", pbn.getPg() == 3);
113 pbn.setPg(-999);
114 assertTrue("did not force page number correctly to 1 (always safe)", pbn.getPg() == 1);
115 pbn.setPg(7);
116 assertTrue("did not set page number correctly to 7", pbn.getPg() == 7);
117 pbn.setPg(999);
118 assertTrue("did not force page number correctly to 1 (always safe)", pbn.getPg() == 1);
119 pbn.setPg(10);
120 assertTrue("did not set page number correctly to 10 (upper limit)", pbn.getPg() == 10);
121 pbn.setPg(1);
122 assertTrue("did not set page number correctly to 1 (lower limit)", pbn.getPg() == 1);
123 // Test the String argument version.
124 pbn.setPg("4");
125 assertTrue("did not set page number correctly to 4", pbn.getPg() == 4);
126 pbn.setPg("broken");
127 assertTrue("did not force page number correctly to 1 (always safe)", pbn.getPg() == 1);
128 // Do the generic tests with current setup.
129 testPBContract(pbn);
130
131 // Test item upper/lower bound algorithm
132 pbn.setNumberOfItems(2 + 3 * WebConsts.MAX_RESULTS_PER_PAGE);
133 assertTrue("wrong result for four pages' worth of items", pbn.getNumberOfPages() == 4);
134 pbn.setPg(1);
135 assertTrue("lower bound for first page wrong", pbn.getStartIndex() == 0);
136 assertTrue("upper bound for first page wrong", pbn.getEndIndex() == WebConsts.MAX_RESULTS_PER_PAGE);
137 pbn.setPg(2);
138 assertTrue("lower bound for second page wrong", pbn.getStartIndex() == WebConsts.MAX_RESULTS_PER_PAGE);
139 assertTrue("upper bound for second page wrong", pbn.getEndIndex() == 2 * WebConsts.MAX_RESULTS_PER_PAGE);
140 pbn.setPg(4);
141 assertTrue("lower bound for last (partial) page wrong", pbn.getStartIndex() == 3 * WebConsts.MAX_RESULTS_PER_PAGE);
142 assertTrue("upper bound for last (partial) page wrong", pbn.getEndIndex() == pbn.getNumberOfItems());
143 // Do the generic tests with current setup.
144 testPBContract(pbn);
145
146 // Test label generation for smallish number of pages
147 // (less than the maximum we can do buttons for in one go).
148 pbn.setNumberOfItems(1 + 2 * WebConsts.MAX_RESULTS_PER_PAGE);
149 assertTrue("Check label generation for page 2", "2".equals(pbn.getPageLabel(2)));
150 assertTrue("Check label generation for page 1", "1".equals(pbn.getPageLabel(1)));
151 assertTrue("Check label generation for page 3", "3".equals(pbn.getPageLabel(3)));
152 final List<String> l1 = pbn.getPageLabels();
153 assertTrue("Check length of label list (3)", l1.size() == 3);
154 assertTrue("Check labels are right for the pages on small list",
155 "1".equals(l1.get(0)) &&
156 "2".equals(l1.get(1)) &&
157 "3".equals(l1.get(2)));
158 // Do the generic tests with current setup.
159 testPBContract(pbn);
160
161 // Test label generation for large number of items
162 // (too many to generate page buttons for all at once).
163 pbn.setNumberOfItems(1 + 2 * WebConsts.MAX_RESULTS_PAGES * WebConsts.MAX_RESULTS_PER_PAGE);
164 assertTrue("Check label generation for page 2", "2".equals(pbn.getPageLabel(2)));
165 assertTrue("Check label generation for page 1", "1".equals(pbn.getPageLabel(1)));
166 assertTrue("Check label generation for page 3", "3".equals(pbn.getPageLabel(3)));
167 assertTrue("Check label generation for page 17: got "+pbn.getPageLabel(17), "17".equals(pbn.getPageLabel(17)));
168 final List<String> l2 = pbn.getPageLabels();
169 assertTrue("Check length of label list for large set", l2.size() <= WebConsts.MAX_RESULTS_PAGES);
170 // Check first and last pages have labels,
171 // and rest are monotonically increasing.
172 assertTrue("Check first and last labels are right for the pages on large list",
173 "1".equals(l1.get(0)) &&
174 String.valueOf(pbn.getNumberOfPages()).equals(l2.get(l2.size() - 1)));
175 int lastP1 = 0;
176 for(final Iterator<String> it = l2.iterator(); it.hasNext(); )
177 {
178 final int n = Integer.parseInt(it.next());
179 assertTrue("Checking for monotonically-increasing list", n > lastP1);
180 lastP1 = n;
181 }
182 // Do the generic tests with current setup.
183 testPBContract(pbn);
184 }
185
186 /**Test the PaginationBeanTree.
187 */
188 public void testTreeFilterBean()
189 {
190 // NOW TEST ON SOME REALISTIC DATA.
191 // Short names of the exhibits.
192 final String SHNAME1 = "cat-sat-on-mat-ANON.htxt";
193 final String SHNAME2 = "CAT-fat-DHD.jpg"; // Note different case of main word.
194 final String SHNAME3 = "dog-in-manger-1-ANON.gif";
195 final String SHNAME4 = "dog-in-manger-2-ANON.gif";
196 final String SHNAME5 = "Zambia-1-ANON.gif";
197
198 // Full names of exhibits.
199 final String EXNAME1 = "memes/" + SHNAME1;
200 final String EXNAME2 = "memes/" + SHNAME2;
201 final String EXNAME3 = "other/" + SHNAME3;
202 final String EXNAME4 = "another/" + SHNAME4;
203 final String EXNAME5 = "art/" + SHNAME5;
204
205 // For now, ignore the effects of attribute words...
206 final List<Name.ExhibitFull> exhibits1 = new ArrayList<Name.ExhibitFull>();
207 exhibits1.add(Name.ExhibitFull.create(EXNAME1));
208 exhibits1.add(Name.ExhibitFull.create(EXNAME2));
209 exhibits1.add(Name.ExhibitFull.create(EXNAME3));
210 exhibits1.add(Name.ExhibitFull.create(EXNAME4));
211 exhibits1.add(Name.ExhibitFull.create(EXNAME5));
212 // Add lots of very similar exhibits for same node that will need lots of pages. */
213 for(int i = 3 + 2 * WebConsts.MAX_RESULTS_PAGES * WebConsts.MAX_RESULTS_PER_PAGE; --i >= 0; )
214 {
215 final String name = "yetanother/dog-in-mangerette-" + i + "-ANON.jpg";
216 exhibits1.add(Name.ExhibitFull.create(name));
217 }
218
219 // Build this Map non-optimised and optimised to check behaviour.
220 for(final boolean optimised : new boolean[]{ false, true })
221 {
222 // First just create an instance of the bean.
223 final PaginationBeanTree pbt = new PaginationBeanTree();
224
225 // Test initial values.
226 assertTrue("default number of items should be zero", pbt.getNumberOfItems() == 0);
227 assertTrue("default page number should be 1", pbt.getPg() == 1);
228 // Do the generic tests with current setup.
229 testPBContract(pbt);
230
231 // Make sure that we can't call setNumberOfItems() directly.
232 try {
233 pbt.setNumberOfItems(43);
234 assertTrue("Should not have been able to call setNumberOfItems()", false);
235 }
236 catch(final Error e) { } // OK: wanted this call vetoed.
237
238 // Make sure that things work with an empty List.
239 final List<CharSequence> emptyList = Collections.emptyList();
240 pbt.setTreeNodeDetails(Name.EMPTY, emptyList);
241 assertTrue("An empty list should mean no items", pbt.getNumberOfItems() == 0);
242
243
244 // Build this map with List nodes.
245 final boolean asSet = false;
246 final Map<Name, Collection<CharSequence>> m1 = TreeFilterBean.computePrefixMap(Collections.unmodifiableList(exhibits1), asSet, optimised);
247
248 // Extract the root node (which is small enough to get on one page).
249 final CharSequence rootPrefix = Name.EMPTY;
250 final Collection<CharSequence> prefixesAndShortExhibitName = m1.get(rootPrefix);
251 assertTrue(prefixesAndShortExhibitName instanceof List);
252 final List<CharSequence> prefixesAndShortExhibitNameL = (List<CharSequence>) prefixesAndShortExhibitName;
253 pbt.setTreeNodeDetails(rootPrefix, prefixesAndShortExhibitNameL);
254 // Note that more items are brought down to the root when optimising...
255 assertEquals("Expect small root node (2 items)", optimised ? 5 : 3, pbt.getNumberOfItems());
256 assertEquals("Expect small root node (1 page)", 1, pbt.getNumberOfPages());
257 // Do the generic tests with current setup.
258 testPBContract(pbt);
259 System.out.println(prefixesAndShortExhibitNameL);
260 // Test root nodes: will vary with optimisation.
261 if(!optimised)
262 {
263 assertEquals("cat-", prefixesAndShortExhibitNameL.get(0).toString());
264 assertEquals("dog-", prefixesAndShortExhibitNameL.get(1).toString());
265 assertEquals("zambia-", prefixesAndShortExhibitNameL.get(2).toString());
266 }
267 else
268 {
269 assertEquals(SHNAME2, prefixesAndShortExhibitNameL.get(0).toString());
270 assertEquals(SHNAME1, prefixesAndShortExhibitNameL.get(1).toString());
271 assertEquals("dog-in-manger-", prefixesAndShortExhibitNameL.get(2).toString());
272 assertEquals("dog-in-mangerette-", prefixesAndShortExhibitNameL.get(3).toString());
273 assertEquals(SHNAME5, prefixesAndShortExhibitNameL.get(4).toString());
274 }
275
276 // Extract the "dog-in-mangerette-" node, which should be huge.
277 final Name dimPrefix = Name.create("dog-in-mangerette-");
278 final Collection<CharSequence> prefixesAndShortExhibitName2 = m1.get(dimPrefix);
279 assertTrue(prefixesAndShortExhibitName2 instanceof List);
280 pbt.setTreeNodeDetails(dimPrefix, (List<CharSequence>) prefixesAndShortExhibitName2);
281 assertTrue("Expect huge root node",
282 pbt.getNumberOfItems() > WebConsts.MAX_RESULTS_PAGES * WebConsts.MAX_RESULTS_PER_PAGE);
283 // Do the generic tests with current setup.
284 testPBContract(pbt);
285
286 // Check entries in "cat-" node have correct position (eg sort order)
287 // in spite of difference in case which might reverse things.
288 final Name catPrefix = Name.create("cat-");
289 final Collection<CharSequence> prefixesAndShortExhibitName3 = m1.get(catPrefix);
290 assertTrue(prefixesAndShortExhibitName3 instanceof List);
291 pbt.setTreeNodeDetails(catPrefix, (List<CharSequence>) prefixesAndShortExhibitName3);
292 // Do the generic tests with current setup.
293 testPBContract(pbt);
294 // Test initial elements for correctness...
295 assertEquals(optimised ? SHNAME2 : "cat-fat-", ((List<CharSequence>) prefixesAndShortExhibitName3).get(0).toString());
296 assertEquals(optimised ? SHNAME1 : "cat-sat-", ((List<CharSequence>) prefixesAndShortExhibitName3).get(1).toString());
297 }
298 }
299
300 /**Test the PaginationBeanTree on a realistic data set from a recent AEP.
301 * This is mainly a test that nothing breaks,
302 * and a sneaky performance test.
303 */
304 public void testTreeFilterBeanOnFullDataSet() throws Exception
305 {
306 // Test performance on a realistic exhibit-name data set.
307 final File lastAEPFile = new File(BackCompatTest.frozenAEPs.get(BackCompatTest.frozenAEPs.size()-1));
308
309 Main.getOut().println("Using captured AEP " + lastAEPFile);
310 final AllExhibitProperties lastAEP =
311 (AllExhibitProperties) FileTools.deserialiseFromFile(lastAEPFile, true);
312
313 // Compute as for the 'all exhibits' browsable tree...
314 final List<ExhibitFull> allExhibitNames = lastAEP.aeid.getAllExhibitNamesSorted();
315 final Map<Name, Collection<CharSequence>> prefixMap;
316
317 // Profile total cost of building a big tree...
318 final ConcurrentMap<StackTraceElement, AtomicInteger> perfCounts = new ConcurrentHashMap<StackTraceElement, AtomicInteger>(1001);
319 final ConcurrentMap<StackTraceElement, ConcurrentMap<StackTraceElement, AtomicInteger>> parentPerfCounts = new ConcurrentHashMap<StackTraceElement, ConcurrentMap<StackTraceElement, AtomicInteger>>(1001);
320 final Thread perfMonitorThread = GenUtils.startThreadPerfMonitor(
321 Thread.currentThread(),
322 perfCounts,
323 parentPerfCounts,
324 "org.hd.", // Capture our code.
325 System.currentTimeMillis() + 1000000, // Monitor thread for at most 1000s.
326 50); // Sample relatively slowly, esp for WinTel development machine.
327 try
328 {
329 prefixMap = TreeFilterBean.computePrefixMap(allExhibitNames, false, true);
330 assertTrue(prefixMap.size() >= allExhibitNames.size());
331 }
332 finally
333 {
334 GenUtils.stopPerfMonitorandDumpSamples(
335 perfMonitorThread,
336 "TreeFilterBean all-exhibits profile",
337 perfCounts,
338 parentPerfCounts,
339 20,
340 GenUtils.systemOutLogger);
341 }
342
343 // Check that we don't break the pagination bean on this (big) data set...
344 // Extract the root node.
345 final CharSequence rootPrefix = Name.EMPTY;
346 final Collection<CharSequence> prefixesAndShortExhibitName = prefixMap.get(rootPrefix);
347 assertTrue(prefixesAndShortExhibitName instanceof List);
348 final List<CharSequence> prefixesAndShortExhibitNameL = (List<CharSequence>) prefixesAndShortExhibitName;
349 final PaginationBeanTree pbt = new PaginationBeanTree();
350 pbt.setTreeNodeDetails(rootPrefix, prefixesAndShortExhibitNameL);
351 // Note that more items are brought down to the root when optimising...
352 // Do the generic tests with current setup.
353 testPBContract(pbt);
354 }
355
356 /**If true, we are very strict and insist that we must use all available buttons.
357 * This is to give users maximum number of useful choices.
358 */
359 private static final boolean MUST_USE_ALL_AVAILABLE_BUTTONS = true;
360
361 /**Do some tests of generic contract that should work on any PaginationBaseBean or sub-class.
362 * The bean must be passed in already set up.
363 * <p>
364 * This may alter some or all of the bean state.
365 */
366 private static void testPBContract(final PaginationBeanBase pbb)
367 {
368 // Test labels of bean in whatever state it arrived...
369 testLabels(pbb);
370
371 // For every possible page value, test label generation and parsing...
372 for(int pg = pbb.getNumberOfPages() + 1; --pg > 0; )
373 {
374 // Test that we can always correctly parse every page label and set the page.
375 for(int j = pbb.getNumberOfPages() + 1; --j > 0; )
376 {
377 assertTrue("must always be able to generate and parse every page label", j == pbb.getPageNumber(pbb.getPageLabel(j)));
378 pbb.setPg(j);
379 assertTrue("must always be able to set any page from a label", j == pbb.getPg());
380 }
381
382 pbb.setPg(pg);
383 assertTrue("Must be able to set current page number", pg == pbb.getPg());
384
385 //System.out.println("labels..."); for(final Iterator it = pbb.getPageLabels().iterator(); it.hasNext(); ) { System.out.println(it.next()); }
386
387 // Test that all the page-button labels represent pages in monotonically-increasing order,
388 // and produce the same labels as getPageLabel() for each individual page.
389 int last = 0;
390 final int pageNumbers[] = pbb.getPageNumbers();
391 final List<String> theseLabels = pbb.getPageLabels();
392 final int lastPageIndex = Math.max(1, pbb.getNumberOfPages());
393 assertTrue("labels and numbers must be same length", pageNumbers.length == theseLabels.size());
394 //System.out.print("Page sequence (current page = "+pg+"):");
395 for(int i = 0; i < pageNumbers.length; ++i)
396 {
397 // Test labels of bean in every possible bean state...
398 testLabels(pbb);
399
400 final int pgNum = pageNumbers[i];
401 //System.out.print(" " + pgNum);
402 assertTrue("all displayed-page numbers must be in range", (pgNum >= 1) && (pgNum <= lastPageIndex));
403 assertTrue("labels must represent monotonically-ascending pages", pgNum > last);
404 final String label = pbb.getPageLabel(pgNum);
405 assertTrue("getPageNumber() must be the inverse of getPageLabel()", pgNum == pbb.getPageNumber(label));
406 final String s = theseLabels.get(i);
407 assertTrue("label set must match individual labels (pg "+pgNum+"): "+s+"/"+label, s.equals(label));
408 last = pgNum;
409 }
410 //System.out.println(" ...end ("+theseLabels.size()+" buttons).");
411 }
412 }
413
414 /**Test some characteristics of the labels obtained from the bean.
415 */
416 private static void testLabels(final PaginationBeanBase pbb)
417 {
418 final List<String> labels = pbb.getPageLabels();
419
420 // Under all circumstances, number of pages and items must be non-negative.
421 assertTrue("Number of items and pages must be non-negative",
422 (pbb.getNumberOfItems() >= 0) && (pbb.getNumberOfPages() >= 0));
423 // The page number must be suitably constrained.
424 assertTrue("Page number must be [1, numberOfPages] if more than zero items: " + pbb.getPg() + " [" + pbb.getNumberOfPages() + "]",
425 (pbb.getPg() >= 1) &&
426 ((pbb.getNumberOfItems() == 0) || (pbb.getPg() <= pbb.getNumberOfPages())));
427 // Under all circumstances, the number of page buttons must be no more than WebConsts.MAX_RESULTS_PAGES.
428 assertTrue("Must not produce too many pages", labels.size() <= WebConsts.MAX_RESULTS_PAGES);
429
430 // We also ensure (though this is in principle less critical)
431 // that at least a substantial fraction of the possible labels are used,
432 // else the user may have to click too many buttons.
433 assertTrue("A reasonable fraction of available buttons must be used",
434 labels.size() >= Math.min(WebConsts.MAX_RESULTS_PAGES, pbb.getNumberOfPages()) / 2);
435
436 // If being strict we insist that all buttons are used.
437 if(MUST_USE_ALL_AVAILABLE_BUTTONS)
438 {
439 assertTrue("All available buttons must be used so as to give the user the shortest path to their target page",
440 labels.size() == Math.min(WebConsts.MAX_RESULTS_PAGES, pbb.getNumberOfPages()));
441 }
442
443 // Test that all the page-button labels are unique.
444 assertTrue("All labels must be unique", (new HashSet<String>(labels)).size() == labels.size());
445 assertTrue("labels and numbers must be same length", pbb.getPageNumbers().length == labels.size());
446 }
447 }