/*
* File: HashFunctionUtilTestHarness.java
* Authors: Kevin R. Dixon
* Company: Sandia National Laboratories
* Project: Cognitive Foundry
*
* Copyright Jan 26, 2011, Sandia Corporation.
* Under the terms of Contract DE-AC04-94AL85000, there is a non-exclusive
* license for use of this work by or on behalf of the U.S. Government.
* Export of this program may require a license from the United States
* Government. See CopyrightHistory.txt for complete details.
*
*/
package gov.sandia.cognition.hash;
import gov.sandia.cognition.math.UnivariateStatisticsUtil;
import gov.sandia.cognition.util.ObjectUtil;
import java.util.ArrayList;
import junit.framework.TestCase;
import java.util.Random;
/**
* Unit tests for HashFunctionTestHarness.
*
* @author krdixon
*/
public abstract class HashFunctionTestHarness
extends TestCase
{
/**
* Random number generator to use for a fixed random seed.
*/
public Random RANDOM = new Random( 1 );
/**
* Default tolerance of the regression tests, {@value}.
*/
public double TOLERANCE = 1e-5;
/**
* Number of samples, {@value}.
*/
public int NUM_SAMPLES = 100;
/**
* Tests for class HashFunctionTestHarness.
* @param testName Name of the test.
*/
public HashFunctionTestHarness(
String testName)
{
super(testName);
}
/**
* Determines if two byte arrays are equal, element-by-element
* @param a1
* a1
* @param a2
* a2
* @return
* true if equal, false if not equal
*/
public static boolean byteArrayEquals(
byte[] a1,
byte[] a2 )
{
if( a1.length != a2.length )
{
return false;
}
for( int i = 0; i < a1.length; i++ )
{
if( a1[i] != a2[i] )
{
return false;
}
}
return true;
}
/**
* Tests the constructors of class HashFunctionTestHarness.
*/
public abstract void testConstructors();
/**
* Creates a new instance of HashFunction
* @return
* new HashFunction to evaluate
*/
public abstract HashFunction createInstance();
/**
* Test of length method, of class HashFunction.
*/
public abstract void testLength();
/**
* Test of evaluate method, of class HashFunction.
*/
public abstract void testEvaluateKnownValues();
/**
* Test of clone method, of class HashFunction.
*/
public void testClone()
{
System.out.println( "clone" );
HashFunction instance = this.createInstance();
HashFunction clone = ObjectUtil.cloneSafe(instance);
for( int n = 0; n < NUM_SAMPLES; n++ )
{
int inputLength = RANDOM.nextInt(100)+10;
byte[] input = new byte[inputLength];
RANDOM.nextBytes(input);
byte[] seed = new byte[ instance.length() ];
RANDOM.nextBytes(seed);
byte[] r1 = instance.evaluate(input, seed);
byte[] r2 = clone.evaluate(input, seed);
assertTrue( byteArrayEquals(r1, r2) );
}
}
/**
* Test of evaluate method, of class HashFunction.
*/
public void testEvaluate()
{
System.out.println("evaluate");
HashFunction instance = this.createInstance();
byte[] seed1 = new byte[ instance.length() ];
RANDOM.nextBytes(seed1);
byte[] seed2 = new byte[ instance.length() ];
RANDOM.nextBytes(seed2);
for( int n = 0; n < NUM_SAMPLES; n++ )
{
int inputLength = RANDOM.nextInt(100)+10;
byte[] input = new byte[inputLength];
RANDOM.nextBytes(input);
byte[] r1 = instance.evaluate(input);
assertEquals( instance.length(), r1.length );
byte[] r2 = instance.evaluate(input, instance.getDefaultSeed() );
assertEquals( instance.length(), r2.length );
assertTrue( byteArrayEquals( r1, r2 ) );
byte[] s1 = instance.evaluate(input, seed1);
assertEquals( instance.length(), s1.length );
byte[] s2 = instance.evaluate(input, seed2);
assertEquals( instance.length(), s2.length );
assertFalse( byteArrayEquals( s1, s2 ) );
byte[] s3 = instance.evaluate(input,seed1 );
assertEquals( instance.length(), s3.length );
assertTrue( byteArrayEquals(s1, s3) );
}
byte[] r1 = instance.evaluate(null);
byte[] r2 = instance.evaluate(null, instance.getDefaultSeed() );
assertTrue( byteArrayEquals(r1, r2) );
byte[] r3 = instance.evaluate(new byte[0] );
byte[] r4 = instance.evaluate(new byte[0], instance.getDefaultSeed() );
assertTrue( byteArrayEquals(r3, r4) );
byte[] s1 = instance.evaluate(null,seed1);
byte[] s2 = instance.evaluate(null,seed2);
assertEquals( instance.length(), s1.length );
assertEquals( instance.length(), s2.length );
byte[] s3 = instance.evaluate(new byte[0],seed1 );
byte[] s4 = instance.evaluate(new byte[0],seed2 );
assertEquals( instance.length(), s3.length );
assertEquals( instance.length(), s4.length );
}
/**
* Test of evaluateInto method, of class HashFunction.
*/
public void testEvaluateInto_byteArr_byteArr()
{
System.out.println("evaluateInto");
HashFunction instance = this.createInstance();
int length = instance.length();
for( int n = 0; n < NUM_SAMPLES; n++ )
{
int inputLength = RANDOM.nextInt(100)+10;
byte[] input = new byte[inputLength];
RANDOM.nextBytes(input);
byte[] r1 = new byte[length];
instance.evaluateInto(input, r1);
byte[] r2 = instance.evaluate(input);
assertEquals( length, r1.length );
assertEquals( length, r2.length );
assertTrue( byteArrayEquals( r1, r2 ) );
byte[] r3 = new byte[length];
instance.evaluateInto(input, r3, instance.getDefaultSeed() );
assertEquals( length, r3.length );
assertTrue( byteArrayEquals(r1, r3) );
}
int inputLength = RANDOM.nextInt(100)+10;
byte[] input = new byte[inputLength];
RANDOM.nextBytes(input);
byte[] r1 = new byte[length];
instance.evaluateInto(null,r1);
byte[] r2 = new byte[length];
instance.evaluateInto(null,r2,instance.getDefaultSeed());
assertTrue( byteArrayEquals(r1, r2) );
instance.evaluateInto(new byte[0],r1 );
instance.evaluateInto(new byte[0],r2,instance.getDefaultSeed());
assertTrue( byteArrayEquals(r1, r2) );
byte[] output = new byte[length+1];
try
{
instance.evaluateInto(input, output);
fail( "Output is the wrong length" );
}
catch (Exception e)
{
System.out.println( "Good: " + e );
}
output = new byte[length-1];
try
{
instance.evaluateInto(input, output);
fail( "Output is the wrong length" );
}
catch (Exception e)
{
System.out.println( "Good: " + e );
}
byte[] badSeed = new byte[ length+1 ];
try
{
instance.evaluateInto(input, new byte[length], badSeed );
fail( "Seed is the wrong length!" );
}
catch (Exception e)
{
System.out.println( "Good: " + e );
}
}
/**
* Test of evaluateInto method, of class HashFunction.
*/
public void testEvaluateInto_3args()
{
System.out.println("evaluateInto");
HashFunction instance = this.createInstance();
int length = instance.length();
byte[] seed1 = new byte[ length ];
RANDOM.nextBytes(seed1);
byte[] seed2 = new byte[ length ];
RANDOM.nextBytes(seed2);
byte[] r1 = new byte[length];
byte[] r2 = new byte[length];
byte[] r3 = new byte[length];
for( int n = 0; n < NUM_SAMPLES; n++ )
{
int inputLength = RANDOM.nextInt(100)+10;
byte[] input = new byte[inputLength];
RANDOM.nextBytes(input);
instance.evaluateInto(input, r1, seed1);
instance.evaluateInto(input, r2, seed2);
instance.evaluateInto(input, r3, seed1);
assertFalse( byteArrayEquals(r1, r2) );
assertTrue( byteArrayEquals(r1, r3) );
assertFalse( byteArrayEquals(r2, r3) );
}
int inputLength = RANDOM.nextInt(100)+10;
byte[] input = new byte[inputLength];
RANDOM.nextBytes(input);
r1 = new byte[length];
instance.evaluateInto(null,r1,seed1);
assertNotNull( r1 );
byte[] output = new byte[length+1];
try
{
instance.evaluateInto(input, output, seed1);
fail( "Output is the wrong length" );
}
catch (Exception e)
{
System.out.println( "Good: " + e );
}
output = new byte[length-1];
try
{
instance.evaluateInto(input, output, seed2);
fail( "Output is the wrong length" );
}
catch (Exception e)
{
System.out.println( "Good: " + e );
}
}
/**
* Computes the relative entropy for the hash function, lower relative
* entropy is better (ideally 0.0).
* @param num
* Number of samples to draw.
* @return
* Average relative entropy of the hash function output on a byte-by-byte
* basis
*/
public double computeRelativeEntropy(
int num )
{
HashFunction hash = this.createInstance();
final int length = hash.length();
final byte[] seed = hash.getDefaultSeed();
long[][] distribution = new long[ length ][];
for( int i = 0; i < length; i++ )
{
distribution[i] = new long[ 256 ];
}
byte[] output = new byte[ length ];
long start = System.currentTimeMillis();
long ht = 0;
for( int n = 0; n < num; n++ )
{
int l = RANDOM.nextInt(1000) + 1;
byte[] input = new byte[ l ];
RANDOM.nextBytes(input);
long hstart = System.currentTimeMillis();
hash.evaluateInto(input, output, seed );
long hstop = System.currentTimeMillis();
ht += hstop-hstart;
for( int i = 0; i < length; i++ )
{
distribution[i][ output[i] & 0xff ]++;
}
}
long stop = System.currentTimeMillis();
System.out.println( "Hash Average Time: " + 1000.0*(ht) / num );
System.out.println( "Total Average Time: " + 1000.0*(stop-start) / num );
ArrayList<Double> al = new ArrayList<Double>( 256 );
for( int i = 0; i < 256; i++ )
{
al.add( 0.0 );
}
double entropy = 0.0;
for( int i = 0; i < distribution.length; i++ )
{
for( int j = 0; j < distribution[i].length; j++ )
{
al.set( j, ((double) distribution[i][j]) / num );
}
// Each byte has a maximum of 8-bits... this is how close
// we got to the 8-bit target!
entropy += 8.0 - UnivariateStatisticsUtil.computeEntropy( al );
}
return entropy / length;
}
/**
* getDefaultSeed
*/
public void testGetDefaultSeed()
{
System.out.println( "getDefaultSeed" );
HashFunction instance = this.createInstance();
byte[] seed = instance.getDefaultSeed();
assertEquals( instance.length(), seed.length );
// Make sure it's not a reference to the actual default seed
assertEquals( seed[0], instance.getDefaultSeed()[0] );
seed[0] += 1;
assertFalse( "Must return a copy of the default seed!",
seed[0] == instance.getDefaultSeed()[0] );
byte[] input = new byte[ RANDOM.nextInt(100) + 100 ];
RANDOM.nextBytes(input);
byte[] r1 = instance.evaluate(input);
byte[] r2 = instance.evaluate(input, instance.getDefaultSeed() );
assertTrue( byteArrayEquals(r1, r2) );
}
}