The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/usr/bin/perl
#
# ParamSpec stuff.
#
use strict;
use utf8;
use Glib ':constants';
use Test::More tests => 312;

# first register some types with which to play below.

Glib::Type->register_enum ('Fish', qw(one two red blue));
Glib::Type->register_flags ('Rain', qw(warm cold light heavy));
Glib::Type->register_object ('Glib::Object', 'Skeezle');


my @params;
my $pspec;

# compares only three decimal places of a floating point number.
sub is_float {
	my ($a, $b, $blurb) = @_;
	is (sprintf ('%.3f', $a),
	    sprintf ('%.3f', $b),
	    $blurb);
}

#
# assumes:
#   name = lc $nick
#   type = Glib::Param::$nick
#   value_type = Glib::$nick (unless you supply a specific one)
#   blurb is set and not ''
#   pspec has not been added to an object class (owner_type is undef)
#
sub pspec_common_ok {
	my ($pspec, $nick, $flags, $value_type) = @_;

	$value_type = "Glib::$nick"
		unless $value_type;

	isa_ok ($pspec, 'Glib::ParamSpec');
	isa_ok ($pspec, "Glib::Param::$nick");
	is ($pspec->get_name, lc $nick, "$nick name");
	is ($pspec->get_nick, $nick, "$nick nick");
	ok ($pspec->get_blurb, "$nick blurb");
	ok ($pspec->get_flags == $flags, "$nick flags"); # overloaded eq
	is ($pspec->get_value_type, $value_type, "$nick value type");
	ok (! $pspec->get_owner_type, "$nick owner type (hasn't been added to a class yet)");

	# for hysterical raisons and backward compatibility, the paramspec
	# objects have four keys in them:
	is ($pspec->{name}, $pspec->get_name, "$nick -> {name}"); # not valid if there's a - in the name!
	is ($pspec->{type}, $pspec->get_value_type, "$nick -> {type}");
	ok ($pspec->{flags} == $pspec->get_flags, "$nick -> {flags}");
	is ($pspec->{descr}, $pspec->get_blurb, "$nick -> {descr}");
}

$pspec = Glib::ParamSpec->boolean ('boolean', 'Boolean',
	                           'Is you is, or is you ain\'t my baby',
	                           TRUE, 'readable');
pspec_common_ok ($pspec, 'Boolean', 'readable');
ok ($pspec->get_default_value, "Boolean default (expect TRUE)");
push @params, $pspec;
{
  $pspec = Glib::ParamSpec->boolean ('boolean-default-false',
				     'Boolean-default-false',
				     'Blurb',
				     FALSE, 'readable');
  is ($pspec->get_default_value, '');  # boolSV style empty '' return
}


$pspec = Glib::ParamSpec->string ('string', 'String',
				  'Stringing you along with NULL default.',
				  undef,
				  'readable');
pspec_common_ok ($pspec, 'String', 'readable', 'Glib::String');
is ($pspec->get_default_value, undef, "String default NULL");
push @params, $pspec;


#
# all of the integer types have the same interface.
#
foreach my $inttype (
  [ 'Char', 'It builds character', -10, 120, 64, 'writable'],
  [ 'UChar', 'Give me sign!  I have no sign!', 10, 250, 128, ['readable', 'writable']],
  [ 'Int', 'most bugs show up in integration', -65535, 65535, 1138, ['readable', 'writable', 'construct']],
  [ 'UInt', 'UInt good enough for her', 256, 2**30, 7879, ['readable', 'writable', 'construct-only']],
  [ 'Long', 'Why the long face?', -10000, 10000, 0, G_PARAM_READWRITE],
  [ 'ULong', 'What do ulong for?', 0, 1000000, 100, G_PARAM_READWRITE],
) {
	my ($nick, $blurb, $min, $max, $default, $flags) = @$inttype;
	my $name = lc $nick;
	$pspec = Glib::ParamSpec->$name ($name, @$inttype);
	pspec_common_ok ($pspec, $nick, $flags);
	is ($pspec->get_minimum, $min, "$nick min");
	is ($pspec->get_maximum, $max, "$nick max");
	is ($pspec->get_default_value, $default, "$nick default");
	push @params, $pspec;
}

#
# floating-point types add get_epsilon to the integer interface.
# we also need to use a more sophisticated comparison of the float
# values, since == is rarely sufficient.
#
foreach my $floattype (
  ['Float', 'In the event of a water landing, your seat coushin may be used as a floation device.', -2.718, 3.141529, 0.707, G_PARAM_READWRITE], 
  ['Double', 'Double your pleasure, double your fun', 1.23456789, 9876543.21, 2.0, G_PARAM_READWRITE],
) {
	my ($nick, $blurb, $min, $max, $default, $flags) = @$floattype;
	my $name = lc $nick;
	$pspec = Glib::ParamSpec->$name ($name, @$floattype);
	pspec_common_ok ($pspec, $nick, $flags);
	is_float ($pspec->get_minimum, $min, "$nick minimum");
	is_float ($pspec->get_maximum, $max, "$nick maximum");
	is_float ($pspec->get_default_value, $default, "$nick default");
	ok ($pspec->get_epsilon > 0.0, "$nick epsilon");
	push @params, $pspec;
}


#
# and now the rest.
#

$pspec = Glib::ParamSpec->enum ('enum', 'Enum',
	                        'U Pluribus Enum.',
	                        'Fish', 'blue', G_PARAM_READWRITE);
pspec_common_ok ($pspec, 'Enum', G_PARAM_READWRITE, 'Fish');
is ($pspec->get_enum_class, 'Fish', 'enum class');
is ($pspec->get_default_value, 'blue', "Enum default");
push @params, $pspec;


$pspec = Glib::ParamSpec->flags ('flags', 'Flags',
	                         'Are people loyal to ideas or to flags?',
	                         'Rain', ['light', 'warm'], G_PARAM_READWRITE);
pspec_common_ok ($pspec, 'Flags', G_PARAM_READWRITE, 'Rain');
is ($pspec->get_flags_class, 'Rain', 'flags class');
ok ($pspec->get_default_value == ['light', 'warm'], 'Flags default');
push @params, $pspec;


$pspec = Glib::ParamSpec->boxed ('boxed', 'Boxed',
	                         'Big things come in little boxes',
	                         # we only know one boxed type at this point.
	                         'Glib::Scalar', G_PARAM_READWRITE);
pspec_common_ok ($pspec, 'Boxed', G_PARAM_READWRITE, 'Glib::Scalar');
is ($pspec->get_default_value, undef, 'Boxed default');
push @params, $pspec;


$pspec = Glib::ParamSpec->object ('object', 'Object',
	                          'I object, Your Honor, that\'s pure conjecture!',
	                          'Skeezle', G_PARAM_READWRITE);
pspec_common_ok ($pspec, 'Object', G_PARAM_READWRITE, 'Skeezle');
is ($pspec->get_default_value, undef, 'Object default');
push @params, $pspec;


$pspec = Glib::ParamSpec->param_spec ('param-spec', 'ParamSpec',
	                              '',
	                              'Glib::Param::Enum', G_PARAM_READWRITE);
isa_ok ($pspec, 'Glib::ParamSpec');
isa_ok ($pspec, 'Glib::Param::Param');
is ($pspec->get_name, 'param_spec', 'Param name (modified)');
is ($pspec->{name}, 'param-spec', 'Param name (unmodified)');
is ($pspec->get_nick, 'ParamSpec', 'Param nick');
is ($pspec->get_blurb, '', 'Param blurb');
ok ($pspec->get_flags == G_PARAM_READWRITE, 'Param flags');
is ($pspec->get_value_type, 'Glib::Param::Enum', 'Param value type');
ok (! $pspec->get_owner_type, 'Param owner type');
is ($pspec->get_default_value, undef, 'Param default');
push @params, $pspec;


$pspec = Glib::ParamSpec->unichar ('unichar', 'Unichar',
	                           'is that like unixsex?',
	                           'ö', qw/readable/);
pspec_common_ok ($pspec, 'Unichar', qw/readable/, 'Glib::UInt');
is ($pspec->get_default_value, 'ö', 'Unichar default');
push @params, $pspec;
{
  $pspec = Glib::ParamSpec->unichar ('unichar-nul', 'Unichar-Nul',
				     'Blurb',
				     "\0", # default
				     qw/readable/);
  is ($pspec->get_default_value, "\0",
      'ParamSpec unichar - default zero byte');

  $pspec = Glib::ParamSpec->unichar ('unichar-nul', 'Unichar-Nul',
				     'Blurb',
				     "0", # default
				     qw/readable/);
  is ($pspec->get_default_value, "0",
      'ParamSpec unichar - default zero digit');
}


#
# specific to the perl bindings
#
$pspec = Glib::ParamSpec->IV ('iv', 'IV',
	                      'This is the same as Int',
	                      -20, 10, -5, G_PARAM_READWRITE);
isa_ok ($pspec, 'Glib::Param::Long', 'IV is actually Long');
is ($pspec->get_default_value, -5, 'IV default');
push @params, $pspec;


$pspec = Glib::ParamSpec->UV ('uv', 'UV',
	                      'This is the same as UInt',
	                      10, 20, 15, G_PARAM_READWRITE);
isa_ok ($pspec, 'Glib::Param::ULong', 'UV is actually ULong');
is ($pspec->get_default_value, 15, 'UV default');
push @params, $pspec;


$pspec = Glib::ParamSpec->scalar ('scalar', 'Scalar',
	                          'This is the same as Boxed',
	                          G_PARAM_READWRITE);
isa_ok ($pspec, 'Glib::Param::Boxed', 'Scalar is actually Boxed');
is ($pspec->get_value_type, 'Glib::Scalar', 'boxed holding scalar');
is ($pspec->get_default_value, undef, 'Scalar default');
push @params, $pspec;



#
# now add all of these properties to an object class and verify that
# the owner types are correct.
#

Glib::Type->register (
	'Glib::Object' => 'Bar',
	properties => \@params
);

foreach (@params) {
	is ($_->get_owner_type, 'Bar', ref($_)." owner type after adding");
}

{
  my $object = Bar->new;

  # exercise default GET_PROPERTY fetching pspec default value
  foreach my $pspec (@params) {
    if ($pspec->get_flags & 'readable') {
      my $pname = $pspec->get_name;
      $object->get($pname);
    }
  }

  is ($object->get_property('unichar'), ord('ö'),
      'get_property() unichar default value (unicode code point number)');
}


SKIP: {
	skip "GParamSpecOverride is new in glib 2.4.0", 27
		unless Glib->CHECK_VERSION (2, 4, 0);

	my $pbase = Glib::ParamSpec->boolean ('obool','obool', 'Blurb',
					      0, G_PARAM_READWRITE);
	is ($pspec->get_redirect_target, undef);

	$pspec = Glib::ParamSpec->override ('over', $pbase);
	isa_ok ($pspec, 'Glib::Param::Override');
	is_deeply ($pspec->get_redirect_target, $pbase);
	{
	  my $pbase = Glib::ParamSpec->boolean ('obool',
						'Obool',
						'pbase blurb',
						0, G_PARAM_READWRITE);
	  is ($pbase->get_default_value, '');
	  is ($pbase->get_redirect_target, undef);

	  # p1 targetting pbase
	  my $p1 = Glib::ParamSpec->override ('over', $pbase);
	  isa_ok ($p1, 'Glib::Param::Override');
	  # is_deeply() because paramspec is GBoxed, so no identical objects
	  is_deeply ($p1->get_redirect_target, $pbase);

	  is ($p1->get_blurb, 'pbase blurb');
	  is ($p1->get_nick,  'Obool');
	  is ($p1->get_default_value, '');

	  # p2 targetting p1
	  my $p2 = Glib::ParamSpec->override ('over-over', $p1);
	  isa_ok ($p2, 'Glib::Param::Override');
	  # is_deeply() because paramspec is GBoxed, so no identical objects
	  is_deeply ($p2->get_redirect_target, $pbase);

	  is ($p2->get_blurb, 'pbase blurb');
	  is ($p2->get_nick,  'Obool');
	  is ($p2->get_default_value, '');
	}

	{
	  my $pbase = Glib::ParamSpec->unichar ('ounichar',
						'Ounichar',
						'pbase blurb',
						'z',
						G_PARAM_READWRITE);
	  is ($pbase->get_default_value, 'z');
	  is ($pbase->get_redirect_target, undef);

	  # p1 targetting pbase
	  my $p1 = Glib::ParamSpec->override ('over', $pbase);
	  isa_ok ($p1, 'Glib::Param::Override');
	  # is_deeply() because paramspec is GBoxed, so no identical objects
	  is_deeply ($p1->get_redirect_target, $pbase);

	  is ($p1->get_blurb, 'pbase blurb');
	  is ($p1->get_nick,  'Ounichar');
	  is ($p1->get_default_value, 'z');

	  # p2 targetting p1
	  my $p2 = Glib::ParamSpec->override ('over-over', $p1);
	  isa_ok ($p2, 'Glib::Param::Override');
	  # is_deeply() because paramspec is GBoxed, so no identical objects
	  is_deeply ($p2->get_redirect_target, $pbase);

	  is ($p2->get_blurb, 'pbase blurb');
	  is ($p2->get_nick,  'Ounichar');
	  is ($p2->get_default_value, 'z');
	}
}

#
# Since this is conditional on version, we don't want to overcomplicate
# the testing logic above.
#
SKIP: {
	skip "GParamSpecGType is new in glib 2.10.0", 18
		unless Glib->CHECK_VERSION (2, 10, 0);
	@params = ();

	$pspec = Glib::ParamSpec->gtype ('object', 'Object Type',
					 "Any object type",
					 Glib::Object::,
					 G_PARAM_READWRITE);
	isa_ok ($pspec, 'Glib::Param::GType');
	isa_ok ($pspec, 'Glib::ParamSpec');
	is ($pspec->get_is_a_type, 'Glib::Object');
	is ($pspec->get_value_type, 'Glib::GType');
	push @params, $pspec;

	$pspec = Glib::ParamSpec->gtype ('type', 'Any type', "Any type",
					 undef, G_PARAM_READWRITE);
	isa_ok ($pspec, 'Glib::Param::GType');
	isa_ok ($pspec, 'Glib::ParamSpec');
	is ($pspec->get_is_a_type, undef);
	is ($pspec->get_value_type, 'Glib::GType');
	push @params, $pspec;

	Glib::Type->register ('Glib::Object' => 'Baz', properties => \@params);

	my $baz = Glib::Object::new ('Baz');
	isa_ok ($baz, 'Glib::Object');
	is ($baz->get ('object'), 'Glib::Object');
	is ($baz->get ('type'), undef);

	$baz = Glib::Object::new ('Baz', object => 'Bar', type => 'Glib::ParamSpec');
	isa_ok ($baz, 'Glib::Object');
	is ($baz->get ('object'), 'Bar');
	is ($baz->get ('type'), 'Glib::ParamSpec');

	$baz->set (type => 'Bar');
	is ($baz->get ('type'), 'Bar');
	$baz->set (type => 'Glib::ParamSpec');
	is ($baz->get ('type'), 'Glib::ParamSpec');

        $baz->set (object => 'Glib::Object');
	is ($baz->get ('object'), 'Glib::Object');
        $baz->set (object => 'Glib::InitiallyUnowned');
	is ($baz->get ('object'), 'Glib::InitiallyUnowned');
}



#
# verify that NULL param specs are handled gracefully
#

my $object = Bar->new;
my $x = $object->get ('param_spec');
is ($x, undef);



#
# value_validate() and value_cmp()
#
{ my $p = Glib::ParamSpec->int ('name','nick','blurb',
                                20, 50, 25, G_PARAM_READWRITE);
  ok (! scalar ($p->value_validate('30')), "value 30 valid");
  my @a = $p->value_validate('30');
  is (@a, 2);
  ok (! $a[0], "value 30 bool no modify (array context)");
  is ($a[1], 30, "value 30 value unchanged");

  my ($modif, $newval) = $p->value_validate(70);
  ok ($modif, 'modify 70 to be in range');
  is ($newval, 50, 'clamp 70 down to be in range');
  ($modif, $newval) = $p->value_validate(-70);
  ok ($modif, 'modify -70 to be in range');
  is ($newval, 20, 'clamp -70 down to be in range');

  is ($p->values_cmp(22, 33), -1);
  is ($p->values_cmp(33, 22), 1);
  is ($p->values_cmp(22, 22), 0);
}