/* ALMA - Atacama Large Millimiter Array * Copyright (c) European Southern Observatory, 2012 * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package alma.acs.util.stringqueue.test; import java.io.File; import java.io.IOException; import java.security.InvalidParameterException; import java.util.Calendar; import java.util.Date; import java.util.Random; import java.util.Vector; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import junit.framework.TestCase; import alma.acs.util.IsoDateFormat; import alma.acs.util.stringqueue.DefaultQueueFileHandlerImpl; import alma.acs.util.stringqueue.TimestampedStringQueue; import alma.acs.util.stringqueue.TimestampedStringQueueFileHandler; /** * @author acaproni * * Tests the notification of {@link TimestampedStringQueue} through {@link IStringQueueFileHandler}. * * @version $Id: StringQueueNotification.java,v 1.5 2012/11/09 17:01:54 acaproni Exp $ * @since ACS 10.2 */ public class StringQueueNotification extends TestCase { /** * The file handler for the test extends {@link DefaultQueueFileHandlerImpl} * to instrument {@link #getNewFile()} and {@link #fileProcessed(File, String, String)} * in order to know when they are really executed. * * @author acaproni * */ public class TestFileHandler extends TimestampedStringQueueFileHandler { /** * The timestamp of the oldest log in cache to compare * with that received in {@link #fileProcessed(File, String, String)}. */ private String oldestDate=null; /** * The timestamp of the youngest log in cache to compare * with that received in {@link #fileProcessed(File, String, String)}. */ private String youngestDate=null; /** * Track the number of notifications received. */ private int receivedNotifications=0; /** * Constructor * * @param maxSize The size of each file of the cache */ public TestFileHandler(long maxSize) { super(maxSize); youngestDate=oldestDate=null; } @Override public File getNewFile() throws IOException { File f=null; String acstmp = System.getProperty("ACS.tmp"); try { // Try to create the file in $ACS_TMP if (!acstmp.endsWith(File.separator)) { acstmp=acstmp+File.separator; } File dir = new File(acstmp); f = File.createTempFile(prefix,".tmp",dir); } catch (IOException ioe) { System.out.println("Error creating a file for cache in "+acstmp); ioe.printStackTrace(System.err); return null; } f.deleteOnExit(); System.out.println("New queue file created with name: "+f.getAbsolutePath()); return f; } @Override public void fileProcessed( File filePointer, String minTime, String maxTime) { // Check if the oldest and youngest dates have been correctly set // otherwise there is no point to check the timestamps if (youngestDate==null || youngestDate.isEmpty()) { throw new IllegalStateException("Youngest timestamp has not been set for comparison"); } if (oldestDate==null || oldestDate.isEmpty()) { throw new IllegalStateException("Oldest timestamp has not been set for comparison"); } assertNotNull("minTime should never be null", minTime); assertNotNull("maxTime should never be null", maxTime); assertTrue("Error deleting "+filePointer.getAbsolutePath(),filePointer.delete()); receivedNotifications++; System.out.println("Notification "+receivedNotifications+" received for file "+filePointer.getAbsolutePath()); if (checkNotification) { System.out.print("Checking for correctness of the notification... "); try { assertEquals("Youngest date differ", youngestDate,minTime); assertEquals("Oldest date differ", oldestDate,maxTime); System.out.println("OK"); } catch (Throwable t) { System.out.println("Ops..."); t.printStackTrace(); } finally { if (notificationArrived!=null) { notificationArrived.countDown(); } } } } /** * Set the dates to compare with those received * in {@link #fileProcessed(File, String, String)}. * * @param young The date of the youngest log in cache * @param old The date of the oldest log in cache */ public void setExpectedDates(String young, String old) { if (young==null || young.isEmpty()) { throw new IllegalArgumentException("Invalid timestamp for youngest log "+young); } if (old==null || old.isEmpty()) { throw new IllegalArgumentException("Invalid timestamp for oldest log "+old); } oldestDate=old; youngestDate=young; } } /** * A class to hold the dates of the log in each cache file. * * @author acaproni */ private class LogDates { /** * The date of the youngest log in cache */ public long minDate; /** * The date of the oldest log in cache */ public long maxDate; /** * Constructor * @param minDate The date of the youngest log in cache * @param maxDate The date of the oldest log in cache */ public LogDates(long minDate, long maxDate) { super(); this.minDate = minDate; this.maxDate = maxDate; } /** * Constructor */ public LogDates() { super(); minDate = -1; maxDate = -1; } /** * Constructor * @param initialTimestamp The date of the youngest and oldest logs in cache */ public LogDates(long initialTimestamp) { super(); minDate = initialTimestamp; maxDate = initialTimestamp; } public void updateTimestamps(long timestamp) { if (minDate==-1 || timestamp<minDate) { minDate=timestamp; } if (maxDate==-1 || timestamp>maxDate) { maxDate=timestamp; } } @Override public String toString() { StringBuilder ret = new StringBuilder(); ret. append('['); if (minDate==-1) { ret.append(minDate); } else { ret.append(IsoDateFormat.formatDate(new Date(minDate))); } ret.append(", "); if (maxDate==-1) { ret.append(maxDate); } else { ret.append(IsoDateFormat.formatDate(new Date(maxDate))); } ret. append(']'); return ret.toString(); } } /** * The cache used to stress the file handler */ private TimestampedStringQueue stringQueue; /** * The size of each file of the cache. * <P> * Note that when this size is reached then a new file is created and a notification * sent to the listener. * <P> * {@link #fileProcessed(File, String, String)} checks the dates received * in the notification with those expected. */ private final long cacheFileSize=1024*50; // 50K /** * The file handler of the test */ private TestFileHandler testFileHandler; /** * The header of each log: the initial part of a log before the timestamp */ private final String logHdrTemplate = "<Debug TimeStamp=\""; /** * Notifications must be checked only when performing a test. * <BR> * In fact when the {@link TimestampedStringQueue} is closed it sends a notification before * deleting a file but in that case the dates must not be tested. */ private volatile boolean checkNotification=false; /** * Set to wait for a notification. * <P> * This is needed because the deletion of the file (and the related * notification) is done by a dedicated thread: see {@link TimestampedStringQueue#run()} */ private volatile CountDownLatch notificationArrived=null; /** * The leading part of each log: the part of a log after the timestamp */ private final String logFooterTemplate = "\" File=\"org.jacorb.orb.dsi.ServerRequest\" Line=\"330\" Routine=\"reply\" Host=\"gas01\" Process=\"CONTROL/ACC/javaContainer\" SourceObject=\"jacorb@CONTROL/ACC/javaContainer\" Thread=\"RequestProcessor-2593\" LogId=\"27932171\"><![CDATA[ServerRequest: reply to getAtmosphericConditions]]></Debug>"; /** * Constructor */ public StringQueueNotification() { super(StringQueueNotification.class.getName()); } @Override protected void setUp() throws Exception { super.setUp(); checkNotification=true; testFileHandler = new TestFileHandler(cacheFileSize); assertNotNull(testFileHandler); assertEquals("Size max size of cache differs from the passed one", testFileHandler.maxFilesSize,cacheFileSize ); stringQueue = new TimestampedStringQueue(testFileHandler,"TIMESTAMP=\""); assertNotNull(stringQueue); stringQueue.start(); System.out.println("Queue started: now the test begins"); } @Override protected void tearDown() throws Exception { checkNotification=false; stringQueue.close(true); System.out.println("Queue closed"); super.tearDown(); } /** * Add timestamped strings until a new file is created then * check if the timestamp reported by the notification * are the right ones * <P> * In this test, strings are added ordered by timestamp. * * @throws Exception */ public void testNotifiedDatesOrdered() throws Exception { System.out.println("testNotifiedDatesOrdered started"); // The date of the first log in the queue // This is also the timestamp of the first log of the first created file long startDate=System.currentTimeMillis(); // The timestamp of the last log added in the queue long lastTimestamp = startDate; LogDates logDates = new LogDates(); LogDates notUsedlogDates = new LogDates(); // Add logs in cache int logsInCache=0; while (stringQueue.getActiveFilesSize()<=1) { addLogToCache(lastTimestamp, logDates, notUsedlogDates); lastTimestamp+=250; logsInCache++; assertEquals(logsInCache, stringQueue.size()); } assertEquals("The queue does NOT contain the expected number of strings!",logsInCache, stringQueue.size()); System.out.println("Stopped pushing; "+stringQueue.getActiveFilesSize()+" files used by the cache to store "+stringQueue.size()+" entries"); // The timestamp expected for thefirst notification are: // startDate: the initial log added in the cache // lastTimestamp-500: in fact 250 is added by the loop and must be subtracted; the other 250 beloengs to the last added log // but this log belongs to the new file testFileHandler.setExpectedDates(IsoDateFormat.formatDate(new Date(startDate)), IsoDateFormat.formatDate(new Date(lastTimestamp-500))); notificationArrived=new CountDownLatch(1); // Pop logs to trigger the notification for (int t=0; t<logsInCache; t++) { assertNotNull(stringQueue.pop()); } assertEquals("The queue should be empty!",0, stringQueue.size()); // wait for the notification System.out.println("testNotifiedDatesOrdered awaiting for notification"); if (!notificationArrived.await(2, TimeUnit.MINUTES)) { // Timeout :-( throw new Exception("Notification (fileProcessed) never called!"); } System.out.println("testNotifiedDatesOrdered done."); } /** * Add logs to the cache until the file handler is notified and then checks the * youngest and oldest date of the logs in cache. * <P> * In this test, logs are added without any order. * * @throws Exception */ public void testNotifiedDatesNotOrdered() throws Exception { System.out.println("testNotifiedDatesNotOrdered"); long timestamp = System.currentTimeMillis(); Random rnd = new Random(System.currentTimeMillis()); // For the new timestamp LogDates logDates = new LogDates(); LogDates notUsedlogDates = new LogDates(); // Add the first log before the loop because it triggers // the creation of a new cache file addLogToCache(timestamp+rnd.nextInt(), new LogDates(), logDates); int logsInCache=1; while (stringQueue.getActiveFilesSize()<=1) { int timeStampInc=rnd.nextInt(); timestamp=timestamp+timeStampInc; addLogToCache(timestamp, logDates,notUsedlogDates); logsInCache++; assertEquals(logsInCache, stringQueue.size()); //System.out.println("\t=>"+timeStampInc+", actual="+formatDate(timestamp)+"youngest="+formatDate(youngestTime)+", oldest="+formatDate(oldestTime)); } System.out.println("Stopped pushing; "+stringQueue.getActiveFilesSize()+" files used by the cache to store "+stringQueue.size()+" entries"); testFileHandler.setExpectedDates(IsoDateFormat.formatDate(new Date(logDates.minDate)), IsoDateFormat.formatDate(new Date(logDates.maxDate))); notificationArrived=new CountDownLatch(1); // Pop logs to trigger the notification while (stringQueue.size()>0) { assertNotNull(stringQueue.pop()); } // wait for the notification System.out.println("testNotifiedDatesNotOrdered awaiting for notification"); if (!notificationArrived.await(2, TimeUnit.MINUTES)) { // Timeout :-( throw new Exception("Notification (fileProcessed) never called!"); } System.out.println("testNotifiedDatesNotOrdered done"); } /** * Up to now we tested the notifications with only one file. * Now we check the correctness of the dates with 3 files * and timestamps generated randomly as in {@link #testNotifiedDatesNotOrdered()}. */ public void testNotificationsWithSeveralFiles() throws Exception { System.out.println("testNotificationsWithSeveralFiles"); // The vector will contain one LogDates for each file of the cache Vector<LogDates> logDatesVector = new Vector<StringQueueNotification.LogDates>(); // The number of logs in each file of the cache int[] logsInCache = new int[3]; long timestamp = System.currentTimeMillis(); Random rnd = new Random(System.currentTimeMillis()); // For the new timestamp LogDates actualFileLogDates = new LogDates(); LogDates newFileLogDates = new LogDates(); while (stringQueue.getActiveFilesSize()<=3) { int timeStampInc=rnd.nextInt(); timestamp=timestamp+timeStampInc; boolean newFileCreated=addLogToCache(timestamp, actualFileLogDates, newFileLogDates); if (newFileCreated) { logDatesVector.add(actualFileLogDates); actualFileLogDates=newFileLogDates; newFileLogDates = new LogDates(); } } // Pop the logs to trigger a notification for each of the files in cache int idx=1; // Skip index 0 created when the first log has been inserted testFileHandler.setExpectedDates( IsoDateFormat.formatDate(new Date(logDatesVector.get(idx).minDate)), IsoDateFormat.formatDate(new Date(logDatesVector.get(idx).maxDate))); while (stringQueue.size()>0) { notificationArrived=new CountDownLatch(1); int filesInCache=stringQueue.getActiveFilesSize(); assertNotNull(stringQueue.pop()); if (filesInCache!=stringQueue.getActiveFilesSize()) { // wait for the notification System.out.println("testNotificationsWithSeveralFiles awaiting for notification"); if (!notificationArrived.await(2, TimeUnit.MINUTES)) { // Timeout :-( throw new Exception("Notification (fileProcessed) never called!"); } idx++; if (idx>=logDatesVector.size()) { continue; } testFileHandler.setExpectedDates( IsoDateFormat.formatDate(new Date(logDatesVector.get(idx).minDate)), IsoDateFormat.formatDate(new Date(logDatesVector.get(idx).maxDate))); } } System.out.println("testNotificationsWithSeveralFiles done"); } /** * Generates a log with the passed time. * <P> * The log returned is in the format <code>logHdrTemplate + timestamp + logFooterTemplate</code> * * @param timestamp The time of the log in msec * @return The log */ private String generateLog(long timestamp) { // 2012-08-04T19:29:45.573 Calendar cal = Calendar.getInstance(); cal.setTimeInMillis(timestamp); return String.format("%s%s%s", logHdrTemplate, IsoDateFormat.formatDate(new Date(timestamp)), logFooterTemplate); } /** * Add a new log to the cache signaling if a new cache file has been created. * <P> * The new log to add is automatically generated by {@link #generateLog(long)} starting from * the passed timestamp. * <P> * The method returns a flag to signal if a new cache file has been created.<BR> * <code>addLogToCache</code> updates the dates in the LogDates objects passed in the constructor depending * if a new file has been created or not. If no new file has been created the the dates are stored in * <code>logDates</code>. If a new file is created then the dates are updated in <code>newLogDates</code>. * * @param timestamp The timestamp of the log to add to the cache * @param logDates The dates of the logs in the file of the cache updated only if no new file has been created * @param newLogDates The dates of the logs in the file of the cache updated only if a new file has been created * @return <code>true</code> if a new cache file has been created * throws Exception if the cache returned an error while adding a new log */ private boolean addLogToCache(long timestamp, LogDates logDates, LogDates newLogDates) throws Exception { if (logDates==null || newLogDates==null) { throw new InvalidParameterException("LogDates params can't be null"); } int numoOfLogFilesBefore=stringQueue.getActiveFilesSize(); String newLog=generateLog(timestamp); stringQueue.push(newLog); int numoOfLogFilesAfter=stringQueue.getActiveFilesSize(); boolean aNewFileHasBeenCreated=numoOfLogFilesBefore!=numoOfLogFilesAfter; if (!aNewFileHasBeenCreated) { logDates.updateTimestamps(timestamp); } else { newLogDates.updateTimestamps(timestamp); } return aNewFileHasBeenCreated; } /** * Check if a new file is created when expected by checking the size of * strings in cache against the max length of the file handler. * It also implicitly test it the queue works with a generic time-stamped string instead of a log. * <P> * For this test the dates are not important but only the total * length of the strings pushed in the queue. * In fact, now we push generic time-stamped strings instead of logs. * <P> * testFilesCreation pushes a set of generic string each of each * has a unique integer ID. When the size of the pushed string * is greater then the max length of the file, we expect the cache to create a new file. * The test will ten also get out all the strings and check for their * integrity, checking the value of their IDs. * * @throws Exception */ public void testFilesCreation() throws Exception { System.out.println("testFilesCreation started"); String strHdr="A generic timestamped string to push in queue "+"TIMESTAMP=\""; String strFooter="\" End of generic String "; assertEquals("A newly created cache should have 0 files!",stringQueue.getActiveFilesSize(), 0); // The size of the strings pushed in the queue long size=0; // The unique ID of each string (the first string in queue has ID=1) int ID=0; // We instantiate another queue for the test because we are not // interested in checking the timestamps (TestFileHandler#fileProcessed would return // error without setting the oldest and newest timestamp and with another queue // this test is simpler) TimestampedStringQueue queue = new TimestampedStringQueue(cacheFileSize, "TIMESTAMP=\""); while (size<=cacheFileSize) { ID++; String now = IsoDateFormat.formatDate(new Date(System.currentTimeMillis())); String strToPush=strHdr+now+strFooter+ID; queue.push(strToPush); size+=strToPush.length(); assertEquals("The size of the cache differs from the number of pushed strings", ID, queue.size()); } // Now there should be 2 files in the cache assertEquals("Wrong number of files in queue",2, queue.getActiveFilesSize()); // Get the strings out and checks their IDs for (int t=1; t<=ID; t++) { String str=queue.pop(); assertNotNull(str); assertEquals("The size of the cache differs from expected", ID-t, queue.size()); // Check the integrity of the string assertTrue(str.startsWith(strHdr)); assertTrue("Str red from queue is ["+str+"] but we expect it to have iD="+t,str.endsWith(strFooter+t)); } // The queue should be empty here assertEquals("The queue should be empty!",0, queue.size()); // And should have no files assertEquals("The queue should have 0 files!",1, queue.getActiveFilesSize()); queue.close(true); System.out.println("testFilesCreation done"); } }