The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/usr/local/bin/perl -w

###############################################################################
# Purpose : Unit test for Hash::Flatten
# Author  : John Alden
# Created : Feb 2002
# CVS     : $Header: /home/cvs/software/cvsroot/hash_flatten/t/hash_flatten.t,v 1.21 2009/05/09 12:42:02 jamiel Exp $
###############################################################################
# -t : trace
# -T : deep trace into modules
###############################################################################

use strict;
use Test::Assertions qw(test);
use Getopt::Std;
use Log::Trace;

use vars qw($opt_t $opt_T);
getopts("tT");

plan tests;

#Compile the code
chdir($1) if($0 =~ /(.*)(\/|\\)(.*)/);
unshift @INC, "./lib", "../lib";

#Override warn() first, then compile
my $buf;
{
	BEGIN {$^W = 0}
	*CORE::GLOBAL::warn = sub {$buf = shift()};
	require Hash::Flatten;
}
ASSERT($INC{'Hash/Flatten.pm'}, 'loaded');

import Log::Trace qw(print) if ($opt_t);
deep_import Log::Trace qw(print) if ($opt_T);

#############################################################
#
# Nested hashes
#
#############################################################

my $data =
{
	'x' => 1,
	'y' => {
		'a' => 2,
		'b' => {
			'p' => 3,
			'q' => 4
		},
	}
};

my $flat_data = {
	'x' => 1,
	'y.a' => 2,
	'y.b.p' => 3,
	'y.b.q' => 4
};

my $flat = Hash::Flatten::flatten($data);
DUMP($flat);
ASSERT EQUAL($flat, $flat_data), 'nested hashes';

my $unflat = Hash::Flatten::unflatten($flat);
DUMP($unflat);
ASSERT EQUAL($unflat, $data), 'nested hashes unflattened';

#############################################################
#
# Nested hashes with weird values
#
#############################################################

my $data =
{
	'x' => 1,
	'0' => {
		'1' => 2,
		'' => {
			'' => 3,
			'q' => 4
		},
	},
	'a' => [1,2,3],
	'' => [4,5,6],
};

my $flat_data = {
	'x' => 1,
	'0.1' => 2,
	'0..' => 3,
	'0..q' => 4,
	'a:0' => 1,
	'a:1' => 2,
	'a:2' => 3,
	':0' => 4,
	':1' => 5,
	':2' => 6,
};

my $flat = Hash::Flatten::flatten($data);
DUMP($flat);
ASSERT EQUAL($flat, $flat_data), 'nested hashes with weird values';

my $unflat = Hash::Flatten::unflatten($flat);
DUMP($unflat);
ASSERT EQUAL($unflat, $data), 'nested hashes with weird values unflattened';

#############################################################
#
# Mixed hashes/arrays
#
#############################################################

my $foo = 'hello';
$data =
{
	'x' => 1,
	'ay' => {
		'a' => 2,
		'b' => {
			'p' => 3,
			'q' => 4
		},
	},
	's' => \\\$foo,
	'y' => [
		'a', 2,
		{
			'baz' => 'bum',
		},
	]
};

$flat_data = {
	'ay*b*p' => 3,
	'ay*b*q' => 4,
	's' => 'hello',
	'ay*a' => 2,
	'y%2*baz' => 'bum',
	'x' => 1,
	'y%0' => 'a',
	'y%1' => 2
};

$flat = Hash::Flatten::flatten($data, {'HashDelimiter' => '*', 'ArrayDelimiter' => '%'});
DUMP($flat);
ASSERT EQUAL($flat, $flat_data), 'heterogeneous structure';

$unflat = Hash::Flatten::unflatten($flat, {'HashDelimiter' => '*', 'ArrayDelimiter' => '%'});
DUMP($unflat);
ASSERT EQUAL($unflat,
	{ ### NB we can't compare to $data here because we flatten out scalar refs
		'x' => 1,
		'y' => [
			'a',
			2,
			{
				'baz' => 'bum'
			}
		],
		'ay' => {
			'a' => 2,
			'b' => {
				 'p' => 3,
				 'q' => 4
			}
		},
		's' => 'hello'
	}
), 'heterogeneous structure unflattened';

#############################################################
#
# Deeply nested arrays
#
#############################################################

$data =
{
	'x' => 1,
	'y' => [
		[
			'a', 'fool', 'is',
		],
		[
			'easily', [ 'parted', 'from' ], 'his'
		],
		'money',
	]
};

$flat_data = {
	'y:1:2' => 'his',
	'x' => 1,
	'y:1:1:0' => 'parted',
	'y:1:1:1' => 'from',
	'y:2' => 'money',
	'y:0:0' => 'a',
	'y:1:0' => 'easily',
	'y:0:1' => 'fool',
	'y:0:2' => 'is'
};

$flat = Hash::Flatten::flatten($data);
DUMP($flat);
ASSERT EQUAL($flat, $flat_data), 'nested arrays';

$unflat = Hash::Flatten::unflatten($flat);
DUMP($unflat);
ASSERT EQUAL($unflat, $data), 'nested arrays unflattened';

#############################################################
#
# Trivial cases
#
#############################################################

$data = {
};

$flat_data = {
};

$flat = Hash::Flatten::flatten($data);
DUMP($flat);
ASSERT EQUAL($flat, $flat_data), 'empty hash';

$unflat = Hash::Flatten::unflatten($flat);
DUMP($unflat);
ASSERT EQUAL($unflat, $data), 'empty hash unflattened';

$data = {
	'x' => 1,
};

$flat_data = {
	'x' => 1,
};

$flat = Hash::Flatten::flatten($data);
DUMP($flat);
ASSERT EQUAL($flat, $flat_data), '1 key';

$unflat = Hash::Flatten::unflatten($flat);
DUMP($unflat);
ASSERT EQUAL($unflat, $data), '1 key unflattened';

#############################################################
#
# Very long delimiters
#
###########################################################

$data =
{
	'x' => 1,
	'ay' => {
		'a' => 2,
		'b' => {
			'p' => 3,
			'q' => 4
		},
	},
	's' => 'hey',
	'y' => [
		'a', 2,	{
			'baz' => 'bum',
		},
	]
};

$flat_data = {
	'x' => 1,
	's' => 'hey',
	'ay*Issa Hash*a' => 2,
	'y%This Is an Array!!%%2*Issa Hash*baz' => 'bum',
	'ay*Issa Hash*b*Issa Hash*p' => 3,
	'y%This Is an Array!!%%0' => 'a',
	'ay*Issa Hash*b*Issa Hash*q' => 4,
	'y%This Is an Array!!%%1' => 2
};

$flat = Hash::Flatten::flatten($data, {'HashDelimiter' => '*Issa Hash*', 'ArrayDelimiter' => '%This Is an Array!!%%'});
DUMP($flat);
ASSERT EQUAL($flat, $flat_data), 'long delimiters';

$unflat = Hash::Flatten::unflatten($flat, {'HashDelimiter' => '*Issa Hash*', 'ArrayDelimiter' => '%This Is an Array!!%%'});
DUMP($unflat);
ASSERT EQUAL($unflat, $data), 'long delimiters unflattened';

###########################################################
#
# Scalar refs, blessed refs etc
#
###########################################################

my $scal = 'scalar';
my $again = 'again!';
$data = bless({
	'x' => bless({'foo'=>'bar'}, 'Foo::Hash'),
	'y' => bless(['f', 'g'], 'Bar::Array'),
	'z' => bless(\$scal, 'Qux:Scalar'),
	'r' => bless(\\\\\$again, 'Qux Ref'),
	'rina' => [\$scal, \\$again],
	'gref' => \*FH,
}, 'Template');

DUMP($data);
$flat_data = {
	'z' => 'scalar',
	'r' => 'again!',
	'x.foo' => 'bar',
	'y:0' => 'f',
	'y:1' => 'g',
	'rina:0' => 'scalar',
	'rina:1' => 'again!',
	'gref' => \*FH,
};

$flat = Hash::Flatten::flatten($data);
DUMP($flat);
ASSERT EQUAL($flat, $flat_data), 'blessed references';

$unflat = Hash::Flatten::unflatten($flat);
DUMP($unflat);
ASSERT EQUAL($unflat, {
	'x' => {
		'foo' => 'bar'
	},
	'y' => [
		'f',
		'g'
	],
	'r' => 'again!',
	'z' => 'scalar',
	'rina' => ['scalar', 'again!'],
	'gref' => \*FH,
}), 'objects and blessed refs unflattened';

###########################################################
#
# OO Interface and callbacks
#
###########################################################

my $counter = 0;
my $o = new Hash::Flatten({
	'OnRefRef' => sub {
		my $v = shift;
		$counter++;
		return $$v; #follow	
	},
	'OnRefScalar' => sub {
		my $v = shift;
		$counter--;
		return $$v; #follow	
	},
	'OnRefGlob' => sub {
		my $v = shift;
		$counter--;
		return "A-GLOB";
	}
});

# Test coderef for handling refs
$flat = $o->flatten({a => \\\\\"x"});
DUMP($flat);
ASSERT($counter == 3, "coderef called $counter times");
$flat = $o->flatten({a => \*FH});
DUMP($flat);
ASSERT($counter == 2 && $flat->{a} eq 'A-GLOB', "globref callback");

###########################################################
#
# Escaping
#
###########################################################

my $orig = {
	a => ['1.1', '1.2', '2.1'],
	'b:c' => {e => '3.1'}	
};
$flat = Hash::Flatten::flatten($orig);
DUMP($flat);
$unflat = Hash::Flatten::unflatten($flat);
DUMP($unflat, $orig);
ASSERT(EQUAL($orig, $unflat), "escaping");

$orig = {'a' => {'A[ESC]B' => 'c[ESC]', 'C.D' => 'd:e'}};
$flat = Hash::Flatten::flatten($orig, {EscapeSequence => '[ESC]'});
DUMP($flat);
$unflat = Hash::Flatten::unflatten($flat, {EscapeSequence => '[ESC]'});
DUMP($unflat, $orig);
ASSERT(EQUAL($orig, $unflat), "custom escape seq");

###########################################################
#
# Error checking
#
###########################################################

ASSERT(DIED( sub{ Hash::Flatten::flatten([1,2,3]) } ) && scalar $@ =~ /1st arg must be a hashref/, "type check in flatten");
ASSERT(DIED( sub{ Hash::Flatten::unflatten([1,2,3]) } ) && scalar $@ =~ /1st arg must be a hashref/, "type check in unflatten");

ASSERT(
	DIED( sub{ Hash::Flatten::flatten({}, {EscapeSequence => '.'}) })
	&& scalar $@ =~ /Hash delimiter cannot contain escape sequence/
, "check hash delim for esc seq");

ASSERT(
	DIED( sub{ Hash::Flatten::flatten({}, {EscapeSequence => ':'}) })
	&& scalar $@ =~ /Array delimiter cannot contain escape sequence/
, "check array delim for esc seq");

$data = {
		'y' => {
			'a' => 2,
			'b' => 3
		},
};
$data->{'y'}->{'c'} = $data;
DUMP($data);
ASSERT( DIED( sub { Hash::Flatten::flatten( $data ) } ), 'recursive data structure detected in hashref');

$data = {
		'y' => {
			'a' => 2,
			'b' => 3
		},
};
$data->{y}->{c} = \$data;
DUMP($data);
ASSERT( DIED( sub { Hash::Flatten::flatten( $data ) } ), "recursive data structure detected in ref-ref");

$data = {
		'y' => {
			'a' => 2,
			'b' => 3
		},
};
$data->{'y'}->{'c'} = [1];
push @{$data->{'y'}->{'c'}}, $data->{'y'}->{'c'};
DUMP($data);
ASSERT( DIED( sub { Hash::Flatten::flatten( $data ) } ), "recursive data structure detected in arrayref");

ASSERT(
	DIED( sub{ Hash::Flatten::flatten({a => \[1,2]}, {OnRefRef => "die"}) })
	&& scalar $@ =~ /is a REF/
, "check ref to ref raises exception");

ASSERT(
	DIED( sub{ Hash::Flatten::flatten({a => \"x"}, {OnRefScalar => "die"}) })
	&& scalar $@ =~ /is a SCALAR/
, "check ref to scalar raises exception");

ASSERT(
	DIED( sub{ Hash::Flatten::flatten({a => \*FH}, {OnRefGlob => "die"}) })
	&& scalar $@ =~ /is a GLOB/
, "check ref to glob raises exception");

my $rv = Hash::Flatten::flatten({a => \[1,2]}, {OnRefRef => "warn"});
DUMP($rv);
TRACE($buf);
ASSERT(scalar $buf =~ /is a REF and will be followed/ && EQUAL($rv, {
	'a:0' => 1,
	'a:1' => 2
}), "warn mode works as expected");

$rv = Hash::Flatten::flatten({a=>"m:o.o", "o:i.n:k" => {a=>1}},{EscapeSequence => "#", DisableEscapes => 0});
DUMP($rv);
ASSERT(
	EQUAL($rv,{a => 'm:o.o','o#:i#.n#:k.a' => 1}),
	"Escapes on, returned escaped hash"
);    
$rv = Hash::Flatten::unflatten({a => 'm:o.o','o#:i#.n#:k.a' => 1},{EscapeSequence => "#", DisableEscapes => 0});
DUMP($rv);
ASSERT(
	EQUAL($rv,{a=>"m:o.o", "o:i.n:k" => {a=>1}}),
	"Escapes on, unescaped hash correctly"
);    

$rv = Hash::Flatten::flatten({a=>"m:o.o", "o:i.n:k" => {a=>1}},{EscapeSequence => "#", DisableEscapes => 1});
DUMP($rv);
ASSERT(
	EQUAL($rv,{a => 'm:o.o','o:i.n:k.a' => 1}),
	"Escapes off, returned nonsense"
);    
$rv = Hash::Flatten::unflatten({a => 'm:o.o','o#:i#.n#:k.a' => 1},{EscapeSequence => "#", DisableEscapes => 1});
DUMP($rv);
ASSERT(
	EQUAL($rv,{a => 'm:o.o','o#' => [{'n#' => [{a => 1}]}]}),
	"Escapes off, didn't unescape hash"
);