aboutsummaryrefslogtreecommitdiffstats
path: root/vespaclient/src/perl/test/Yahoo/Vespa
diff options
context:
space:
mode:
authorJon Bratseth <bratseth@yahoo-inc.com>2016-06-15 23:09:44 +0200
committerJon Bratseth <bratseth@yahoo-inc.com>2016-06-15 23:09:44 +0200
commit72231250ed81e10d66bfe70701e64fa5fe50f712 (patch)
tree2728bba1131a6f6e5bdf95afec7d7ff9358dac50 /vespaclient/src/perl/test/Yahoo/Vespa
Publish
Diffstat (limited to 'vespaclient/src/perl/test/Yahoo/Vespa')
-rw-r--r--vespaclient/src/perl/test/Yahoo/Vespa/ArgParserTest.pl313
-rw-r--r--vespaclient/src/perl/test/Yahoo/Vespa/Bin/GetClusterStateTest.pl65
-rw-r--r--vespaclient/src/perl/test/Yahoo/Vespa/Bin/GetNodeStateTest.pl71
-rw-r--r--vespaclient/src/perl/test/Yahoo/Vespa/Bin/SetNodeStateTest.pl129
-rw-r--r--vespaclient/src/perl/test/Yahoo/Vespa/ClusterControllerTest.pl49
-rw-r--r--vespaclient/src/perl/test/Yahoo/Vespa/ConsoleOutputTest.pl47
-rw-r--r--vespaclient/src/perl/test/Yahoo/Vespa/HttpTest.pl140
-rw-r--r--vespaclient/src/perl/test/Yahoo/Vespa/JsonTest.pl67
-rw-r--r--vespaclient/src/perl/test/Yahoo/Vespa/Mocks/ClusterControllerMock.pm258
-rw-r--r--vespaclient/src/perl/test/Yahoo/Vespa/Mocks/HttpClientMock.pm55
-rw-r--r--vespaclient/src/perl/test/Yahoo/Vespa/Mocks/HttpServerMock.pm156
-rw-r--r--vespaclient/src/perl/test/Yahoo/Vespa/Mocks/VespaModelMock.pm96
-rw-r--r--vespaclient/src/perl/test/Yahoo/Vespa/VespaModelTest.pl63
13 files changed, 1509 insertions, 0 deletions
diff --git a/vespaclient/src/perl/test/Yahoo/Vespa/ArgParserTest.pl b/vespaclient/src/perl/test/Yahoo/Vespa/ArgParserTest.pl
new file mode 100644
index 00000000000..78924f1bdcc
--- /dev/null
+++ b/vespaclient/src/perl/test/Yahoo/Vespa/ArgParserTest.pl
@@ -0,0 +1,313 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+use Test::More;
+
+BEGIN { use_ok( 'Yahoo::Vespa::ArgParser' ); }
+require_ok( 'Yahoo::Vespa::ArgParser' );
+
+BEGIN { *ArgParser:: = *Yahoo::Vespa::ArgParser:: }
+
+use TestUtils::OutputCapturer;
+
+TestUtils::OutputCapturer::useColors(1);
+
+&testSyntaxPage();
+
+TestUtils::OutputCapturer::useColors(0);
+
+&testStringOption();
+&testIntegerOption();
+&testHostOption();
+&testPortOption();
+&testFlagOption();
+&testCountOption();
+&testComplexParsing();
+&testArguments();
+
+done_testing();
+
+exit(0);
+
+sub testSyntaxPage {
+ # Empty
+ ArgParser::writeSyntaxPage();
+ my $expected = <<EOS;
+Usage: ArgParserTest.pl
+EOS
+ isOutput($expected, '', 'Empty syntax page');
+
+ # Built in only
+ Yahoo::Vespa::ArgParser::registerInternalParameters();
+ ArgParser::writeSyntaxPage();
+ $expected = <<EOS;
+Usage: ArgParserTest.pl [Options]
+
+Options:
+ -h --help : Show this help page.
+ -v : Create more verbose output.
+ -s : Create less verbose output.
+ --show-hidden : Also show hidden undocumented debug options.
+EOS
+ isOutput($expected, '', 'Syntax page with default args');
+
+ # Actual example
+ ArgParser::initialize();
+
+ setProgramBinaryName("testprog");
+ setProgramDescription(
+ "This is a multiline description of what the program is that "
+ . "should be split accordingly to look nice. For now probably hard "
+ . "coded, but can later be extended to detect terminal width.");
+ my $arg;
+ setArgument(\$arg, "Test Arg", "This argument is not used for anything.",
+ OPTION_REQUIRED);
+ my $optionalArg;
+ setArgument(\$arg, "Another Test Arg",
+ "This argument is not used for anything either.");
+
+ setOptionHeader("My prog headers. Also a long line just to check that it "
+ . "is also split accordingly.");
+ my $stringval;
+ my $flag;
+ my $intval;
+ setStringOption(['string', 'j'], \$stringval, "A random string");
+ setFlagOption(['flag', 'f'], \$flag, "A flag option with a pretty long "
+ . "description that might need to be split into multiple lines.");
+ setOptionHeader("More options");
+ setIntegerOption(['integer', 'i'], \$intval, "A secret integer option.",
+ OPTION_SECRET);
+ Yahoo::Vespa::ArgParser::registerInternalParameters();
+ ArgParser::writeSyntaxPage();
+ $expected = <<EOS;
+This is a multiline description of what the program is that should be split
+accordingly to look nice. For now probably hard coded, but can later be
+extended to detect terminal width.
+
+Usage: testprog [Options] <Test Arg> [Another Test Arg]
+
+Arguments:
+ Test Arg : This argument is not used for anything.
+ Another Test Arg : This argument is not used for anything either.
+
+Options:
+ -h --help : Show this help page.
+ -v : Create more verbose output.
+ -s : Create less verbose output.
+ --show-hidden : Also show hidden undocumented debug options.
+
+My prog headers. Also a long line just to check that it is also split
+accordingly.
+ --string -j : A random string
+ --flag -f : A flag option with a pretty long description that might need
+ to be split into multiple lines.
+EOS
+ isOutput($expected, '', 'Actual syntax page example');
+
+ ArgParser::setShowHidden(1);
+ ArgParser::writeSyntaxPage();
+ $expected = <<EOS;
+This is a multiline description of what the program is that should be split
+accordingly to look nice. For now probably hard coded, but can later be
+extended to detect terminal width.
+
+Usage: testprog [Options] <Test Arg> [Another Test Arg]
+
+Arguments:
+ Test Arg : This argument is not used for anything.
+ Another Test Arg : This argument is not used for anything either.
+
+Options:
+ -h --help : Show this help page.
+ -v : Create more verbose output.
+ -s : Create less verbose output.
+ --show-hidden : Also show hidden undocumented debug options.
+
+My prog headers. Also a long line just to check that it is also split
+accordingly.
+ --string -j : A random string
+ --flag -f : A flag option with a pretty long description that might need
+ to be split into multiple lines.
+
+More options
+ --integer -i : A secret integer option.
+
+ --nocolors : Do not use ansi colors in print.
+EOS
+ isOutput($expected, '', 'Actual syntax page example with hidden');
+}
+
+sub setUpParseTest {
+ Yahoo::Vespa::ArgParser::initialize();
+}
+
+sub parseFail {
+ my ($optstring, $expectedError) = @_;
+ my @args = split(/\s+/, $optstring);
+ my $name = $expectedError;
+ chomp $name;
+ if (length $name > 40 && $name =~ /^(.{20,70}?)\./) {
+ $name = $1;
+ } elsif (length $name > 55 && $name =~ /^(.{40,55})\s/) {
+ $name = $1;
+ }
+ ok( !ArgParser::parseCommandLineArguments(\@args),
+ "Expected parse failure: $name");
+ isOutput('', $expectedError, $name);
+}
+
+sub parseSuccess {
+ my ($optstring, $testname) = @_;
+ my @args = split(/\s+/, $optstring);
+ ok( ArgParser::parseCommandLineArguments(\@args),
+ "Expected parse success: $testname");
+ isOutput('', '', $testname);
+}
+
+sub testStringOption {
+ &setUpParseTest();
+ my $val;
+ setStringOption(['s'], \$val, 'foo');
+ parseFail("-s", "Too few arguments for option 's'\.\n");
+ ok( !defined $val, 'String value unset on failure' );
+ parseSuccess("-s foo", "String option");
+ ok( $val eq 'foo', "String value set" );
+}
+
+sub testIntegerOption {
+ &setUpParseTest();
+ my $val;
+ setIntegerOption(['i'], \$val, 'foo');
+ parseFail("-i", "Too few arguments for option 'i'\.\n");
+ ok( !defined $val, 'Integer value unset on failure' );
+ parseFail("-i foo", "Invalid value 'foo' given to integer option 'i'\.\n");
+ parseFail("-i 0.5", "Invalid value '0.5' given to integer option 'i'\.\n");
+ parseSuccess("-i 5", "Integer option");
+ ok( $val == 5, "Integer value set" );
+ # Don't allow numbers as first char in id, so this can be detected as
+ # argument for integer.
+ parseSuccess("-i -8", "Negative integer option");
+ ok( $val == -8, "Integer value set" );
+ # Test big numbers
+ parseSuccess("-i 8000000000", "Big integer option");
+ ok( $val / 1000000 == 8000, "Integer value set" );
+ parseSuccess("-i -8000000000", "Big negative integer option");
+ ok( $val / 1000000 == -8000, "Integer value set" );
+}
+
+sub testHostOption {
+ &setUpParseTest();
+ my $val;
+ setHostOption(['h'], \$val, 'foo');
+ parseFail("-h", "Too few arguments for option 'h'\.\n");
+ ok( !defined $val, 'Host value unset on failure' );
+ parseFail("-h 5", "Invalid host '5' given to option 'h'\. Not a valid host\n");
+ parseFail("-h non.existing.host.no", "Invalid host 'non.existing.host.no' given to option 'h'\. Not a valid host\n");
+ parseSuccess("-h localhost", "Host option set");
+ is( $val, 'localhost', 'Host value set' );
+}
+
+sub testPortOption {
+ &setUpParseTest();
+ my $val;
+ setPortOption(['p'], \$val, 'foo');
+ parseFail("-p", "Too few arguments for option 'p'\.\n");
+ ok( !defined $val, 'Host value unset on failure' );
+ parseFail("-p -1", "Invalid value '-1' given to port option 'p'\. Must be an unsigned 16 bit\ninteger\.\n");
+ parseFail("-p 65536", "Invalid value '65536' given to port option 'p'\. Must be an unsigned 16 bit\ninteger\.\n");
+ parseSuccess("-p 65535", "Port option set");
+ is( $val, 65535, 'Port value set' );
+}
+
+sub testFlagOption {
+ &setUpParseTest();
+ my $val;
+ setFlagOption(['f'], \$val, 'foo');
+ setFlagOption(['g'], \$val2, 'foo', OPTION_INVERTEDFLAG);
+ parseFail("-f 3", "Unhandled argument '3'\.\n");
+ parseSuccess("-f", "First flag option set");
+ is( $val, 1, 'Flag value set' );
+ is( $val2, 1, 'Flag value set' );
+ parseSuccess("-f", "First flag option reset");
+ is( $val, 1, 'Flag value set' );
+ is( $val2, 1, 'Flag value set' );
+ parseSuccess("-g", "Second flag option set");
+ is( $val, 0, 'Flag value set' );
+ is( $val2, 0, 'Flag value set' );
+ parseSuccess("-fg", "Both flag options set");
+ is( $val, 1, 'Flag value set' );
+ is( $val2, 0, 'Flag value set' );
+}
+
+sub testCountOption {
+ &setUpParseTest();
+ my $val;
+ setUpCountingOption(['u'], \$val, 'foo');
+ setDownCountingOption(['d'], \$val, 'foo');
+ parseSuccess("", "Count not set");
+ ok( !defined $val, 'Count value not set if not specified' );
+ parseSuccess("-u", "Counting undefined");
+ is( $val, 1, 'Count value set' );
+ parseSuccess("-d", "Counting undefined - down");
+ is( $val, -1, 'Count value set' );
+ parseSuccess("-uuuud", "Counting both ways");
+ is( $val, 3, 'Count value set' );
+}
+
+sub testComplexParsing {
+ &setUpParseTest();
+ my $count;
+ my $int;
+ my $string;
+ setUpCountingOption(['u', 'up'], \$count, 'foo');
+ setIntegerOption(['i', 'integer'], \$int, 'bar');
+ setStringOption(['s', 'string'], \$string, 'baz');
+ parseSuccess("-uis 3 foo", "Complex parsing managed");
+ is( $count, 1, 'count counted' );
+ is( $int, 3, 'integer set' );
+ is( $string, 'foo', 'string set' );
+ parseSuccess("-uiusi 3 foo 5", "Complex parsing managed 2");
+ is( $count, 2, 'count counted' );
+ is( $int, 5, 'integer set' );
+ is( $string, 'foo', 'string set' );
+ parseSuccess("-s -i foo -u 3", "Complex parsing managed 3");
+ is( $count, 1, 'count counted' );
+ is( $int, 3, 'integer set' );
+ is( $string, 'foo', 'string set' );
+}
+
+sub testArguments {
+ &testOptionalArgument();
+ &testRequiredArgument();
+ &testRequiredArgumentAfterOptional();
+}
+
+sub testOptionalArgument {
+ &setUpParseTest();
+ my $val;
+ setArgument(\$val, "Name", "Description");
+ parseSuccess("", "Unset optional argument");
+ ok( !defined $val, "Argument unset if not specified" );
+ parseSuccess("myval", "Optional argument set");
+ is( $val, 'myval', 'Optional argument set to correct value' );
+}
+
+sub testRequiredArgument {
+ &setUpParseTest();
+ my $val;
+ setArgument(\$val, "Name", "Description", OPTION_REQUIRED);
+ parseFail("", "Argument Name is required but not specified\.\n");
+ ok( !defined $val, "Argument unset on failure" );
+ parseSuccess("myval", "Required argument set");
+ is( $val, 'myval', 'Required argument set to correct value' );
+}
+
+sub testRequiredArgumentAfterOptional {
+ &setUpParseTest();
+ my ($val, $val2);
+ setArgument(\$val, "Name", "Description");
+ eval {
+ setArgument(\$val2, "Name2", "Description2", OPTION_REQUIRED);
+ };
+ like( $@, qr/Cannot add required argument after optional/,
+ 'Fails adding required arg after optional' );
+}
diff --git a/vespaclient/src/perl/test/Yahoo/Vespa/Bin/GetClusterStateTest.pl b/vespaclient/src/perl/test/Yahoo/Vespa/Bin/GetClusterStateTest.pl
new file mode 100644
index 00000000000..3339d872de5
--- /dev/null
+++ b/vespaclient/src/perl/test/Yahoo/Vespa/Bin/GetClusterStateTest.pl
@@ -0,0 +1,65 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+use Test::More;
+use strict;
+use warnings;
+
+BEGIN { use_ok( 'Yahoo::Vespa::Bin::GetClusterState' ); }
+require_ok( 'Yahoo::Vespa::Bin::GetClusterState' );
+
+use TestUtils::VespaTest;
+use Yahoo::Vespa::Mocks::ClusterControllerMock;
+use Yahoo::Vespa::Mocks::VespaModelMock;
+
+# Set which application is called on assertRun / assertRunMatches calls
+setApplication( \&getClusterState );
+
+useColors(0);
+
+&testSimple();
+&testSyntaxPage();
+&testClusterDown();
+
+done_testing();
+
+exit(0);
+
+sub testSimple {
+ my $stdout = <<EOS;
+
+Cluster books:
+books/storage/0: down
+books/storage/1: up
+
+Cluster music:
+music/distributor/0: down
+music/distributor/1: up
+music/storage/0: retired
+EOS
+ assertRun("Default - no arguments", "", 0, $stdout, "");
+}
+
+sub testClusterDown {
+ Yahoo::Vespa::Mocks::ClusterControllerMock::setClusterDown();
+ Yahoo::Vespa::ClusterController::init();
+ Yahoo::Vespa::Bin::GetClusterState::init();
+ my $stdout = <<EOS;
+
+Cluster books:
+books/storage/0: down
+books/storage/1: up
+
+Cluster music is down. Too few nodes available.
+music/distributor/0: down
+music/distributor/1: up
+music/storage/0: retired
+EOS
+ assertRun("Music cluster down", "", 0, $stdout, "");
+}
+
+sub testSyntaxPage {
+ my $stdout = <<EOS;
+EOS
+ my $pat = qr/^Get the cluster state of a given cluster.*Usage:.*GetClusterState.*Options.*--help.*/s;
+ assertRunMatches("Syntax page", "--help", 1, $pat, qr/^$/);
+}
diff --git a/vespaclient/src/perl/test/Yahoo/Vespa/Bin/GetNodeStateTest.pl b/vespaclient/src/perl/test/Yahoo/Vespa/Bin/GetNodeStateTest.pl
new file mode 100644
index 00000000000..86cff2b28b3
--- /dev/null
+++ b/vespaclient/src/perl/test/Yahoo/Vespa/Bin/GetNodeStateTest.pl
@@ -0,0 +1,71 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+use Test::More;
+use strict;
+use warnings;
+
+BEGIN { use_ok( 'Yahoo::Vespa::Bin::GetNodeState' ); }
+require_ok( 'Yahoo::Vespa::Bin::GetNodeState' );
+
+use TestUtils::VespaTest;
+use Yahoo::Vespa::Mocks::ClusterControllerMock;
+use Yahoo::Vespa::Mocks::VespaModelMock;
+
+useColors(0);
+
+# Set which application is called on assertRun / assertRunMatches calls
+setApplication( \&getNodeState );
+
+&testSimple();
+&testSyntaxPage();
+&testRetired();
+
+done_testing();
+
+exit(0);
+
+sub testSimple {
+ my $stdout = <<EOS;
+Shows the various states of one or more nodes in a Vespa Storage cluster. There
+exist three different type of node states. They are:
+
+ Unit state - The state of the node seen from the cluster controller.
+ User state - The state we want the node to be in. By default up. Can be
+ set by administrators or by cluster controller when it
+ detects nodes that are behaving badly.
+ Generated state - The state of a given node in the current cluster state.
+ This is the state all the other nodes know about. This
+ state is a product of the other two states and cluster
+ controller logic to keep the cluster stable.
+
+books/storage.0:
+Unit: down: Not in slobrok
+Generated: down: Not seen
+User: down: default
+
+music/distributor.0:
+Unit: up: Now reporting state U
+Generated: down: Setting it down
+User: down: Setting it down
+EOS
+ assertRun("Default - no arguments", "", 0, $stdout, "");
+}
+
+sub testRetired {
+ setLocalHost("other.host.yahoo.com");
+ my $stdout = <<EOS;
+
+music/storage.0:
+Unit: up: Now reporting state U
+Generated: retired: Stop using
+User: retired: Stop using
+EOS
+ assertRun("Other node", "-c music -t storage -i 0 -s", 0, $stdout, "");
+}
+
+sub testSyntaxPage {
+ my $stdout = <<EOS;
+EOS
+ my $pat = qr/^Retrieve the state of one or more.*Usage:.*GetNodeState.*Options.*--help.*/s;
+ assertRunMatches("Syntax page", "--help", 1, $pat, qr/^$/);
+}
diff --git a/vespaclient/src/perl/test/Yahoo/Vespa/Bin/SetNodeStateTest.pl b/vespaclient/src/perl/test/Yahoo/Vespa/Bin/SetNodeStateTest.pl
new file mode 100644
index 00000000000..1c6f4180dab
--- /dev/null
+++ b/vespaclient/src/perl/test/Yahoo/Vespa/Bin/SetNodeStateTest.pl
@@ -0,0 +1,129 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+use Test::More;
+use strict;
+use warnings;
+
+BEGIN { use_ok( 'Yahoo::Vespa::Bin::SetNodeState' ); }
+require_ok( 'Yahoo::Vespa::Bin::SetNodeState' );
+
+use TestUtils::VespaTest;
+use Yahoo::Vespa::Mocks::ClusterControllerMock;
+use Yahoo::Vespa::Mocks::VespaModelMock;
+
+# Set which application is called on assertRun / assertRunMatches calls
+setApplication( \&setNodeState );
+
+&testSimple();
+&testSyntaxPage();
+&testHelp();
+&testDownState();
+&testDownFailure();
+&testDefaultMaintenanceFails();
+&testForcedMaintenanceSucceeds();
+
+done_testing();
+
+exit(0);
+
+sub testSimple {
+ my $stdout = <<EOS;
+Set user state for books/storage/0 to 'up' with reason ''
+Set user state for music/distributor/0 to 'up' with reason ''
+EOS
+ assertRun("Default - Min arguments", "up", 0, $stdout, "");
+}
+
+sub testSyntaxPage {
+ my $stdout = <<EOS;
+EOS
+ my $pat = qr/^Set the user state of a node.*Usage:.*SetNodeState.*Arguments:.*Options:.*--help.*/s;
+ assertRunMatches("Syntax page", "--help", 1, $pat, qr/^$/);
+}
+
+sub testHelp {
+ my $stdout = <<EOS;
+Set the user state of a node. This will set the generated state to the user
+state if the user state is "better" than the generated state that would have
+been created if the user state was up. For instance, a node that is currently
+in initializing state can be forced into down state, while a node that is
+currently down can not be forced into retired state, but can be forced into
+maintenance state.
+
+Usage: SetNodeStateTest.pl [Options] <Wanted State> [Description]
+
+Arguments:
+ Wanted State : User state to set. This must be one of up, down, maintenance or
+ retired.
+ Description : Give a reason for why you are altering the user state, which
+ will show up in various admin tools. (Use double quotes to give
+ a reason with whitespace in it)
+
+Options:
+ -h --help : Show this help page.
+ -v : Create more verbose output.
+ -s : Create less verbose output.
+ --show-hidden : Also show hidden undocumented debug options.
+
+Node selection options. By default, nodes running locally will be selected:
+ -c --cluster : Cluster name of cluster to query. If unspecified,
+ and vespa is installed on current node, information
+ will be attempted auto-extracted
+ -f --force : Force the execution of a dangerous command.
+ -t --type : Node type to query. This can either be 'storage' or
+ 'distributor'. If not specified, the operation will
+ show state for all types.
+ -i --index : The node index to show state for. If not specified,
+ all nodes found running on this host will be shown.
+
+Config retrieval options:
+ --config-server : Host name of config server to query
+ --config-server-port : Port to connect to config server on
+ --config-request-timeout : Timeout of config request
+EOS
+
+ assertRun("Help text", "-h", 1, $stdout, "");
+}
+
+sub testDownState {
+ my $stdout = <<EOS;
+Set user state for books/storage/0 to 'down' with reason 'testing'
+Set user state for music/distributor/0 to 'down' with reason 'testing'
+EOS
+ assertRun("Down state", "down testing", 0, $stdout, "");
+}
+
+sub testDownFailure {
+ $Yahoo::Vespa::Mocks::ClusterControllerMock::forceInternalServerError = 1;
+
+ my $stderr = <<EOS;
+Failed to set node state for node books/storage/0: 500 Internal Server Error
+(forced)
+EOS
+
+ assertRun("Down failure", "--nocolors down testing", 1, "", $stderr);
+
+ $Yahoo::Vespa::Mocks::ClusterControllerMock::forceInternalServerError = 0;
+}
+
+sub testDefaultMaintenanceFails {
+ my $stderr = <<EOS;
+Setting the distributor to maintenance mode may have severe consequences for
+feeding!
+Please specify -t storage to only set the storage node to maintenance mode, or
+-f to override this error.
+EOS
+
+ assertRun("Default maintenance fails", "--nocolors maintenance testing",
+ 1, "", $stderr);
+}
+
+sub testForcedMaintenanceSucceeds {
+ my $stdout = <<EOS;
+Set user state for books/storage/0 to 'maintenance' with reason 'testing'
+Set user state for music/distributor/0 to 'maintenance' with reason 'testing'
+EOS
+
+ assertRun("Forced maintenance succeeds", "-f maintenance testing",
+ 0, $stdout, "");
+}
diff --git a/vespaclient/src/perl/test/Yahoo/Vespa/ClusterControllerTest.pl b/vespaclient/src/perl/test/Yahoo/Vespa/ClusterControllerTest.pl
new file mode 100644
index 00000000000..c70d7287566
--- /dev/null
+++ b/vespaclient/src/perl/test/Yahoo/Vespa/ClusterControllerTest.pl
@@ -0,0 +1,49 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+use Test::More;
+use Data::Dumper;
+
+BEGIN { use_ok( 'Yahoo::Vespa::ClusterController' ); }
+require_ok( 'Yahoo::Vespa::ClusterController' );
+
+use TestUtils::OutputCapturer;
+use Yahoo::Vespa::Mocks::ClusterControllerMock;
+use Yahoo::Vespa::Mocks::VespaModelMock;
+
+Yahoo::Vespa::ConsoleOutput::setVerbosity(0); # Squelch output when running test
+detectClusterController();
+Yahoo::Vespa::ConsoleOutput::setVerbosity(3);
+
+my $cclist = Yahoo::Vespa::ClusterController::getClusterControllers();
+is( scalar @$cclist, 1, "Cluster controllers detected" );
+is( $$cclist[0]->host, 'testhost.yahoo.com', 'Host autodetected' );
+is( $$cclist[0]->port, 19050, 'Port autodetected' );
+
+is( join (' - ', Yahoo::Vespa::ClusterController::listContentClusters()),
+ "music - books", 'Content clusters' );
+
+my $state = getContentClusterState('music');
+
+$Data::Dumper::Indent = 1;
+# print Dumper($state);
+
+is( $state->globalState, 'up', 'Generated state for music' );
+
+is( $state->distributor->{'0'}->unit->state, 'up', 'Unit state for music' );
+is( $state->distributor->{'1'}->unit->state, 'up', 'Unit state for music' );
+is( $state->storage->{'0'}->unit->state, 'up', 'Unit state for music' );
+is( $state->storage->{'1'}->unit->state, 'up', 'Unit state for music' );
+is( $state->distributor->{'0'}->generated->state, 'down', 'Generated state' );
+is( $state->distributor->{'1'}->generated->state, 'up', 'Generated state' );
+is( $state->storage->{'0'}->generated->state, 'retired', 'Generated state' );
+is( $state->storage->{'1'}->generated->state, 'up', 'Generated state' );
+is( $state->distributor->{'0'}->user->state, 'down', 'User state' );
+is( $state->distributor->{'1'}->user->state, 'up', 'User state' );
+is( $state->storage->{'0'}->user->state, 'retired', 'User state' );
+is( $state->storage->{'1'}->user->state, 'up', 'User state' );
+
+is( $state->storage->{'1'}->unit->reason, 'Now reporting state U', 'Reason' );
+
+done_testing();
+
+exit(0);
diff --git a/vespaclient/src/perl/test/Yahoo/Vespa/ConsoleOutputTest.pl b/vespaclient/src/perl/test/Yahoo/Vespa/ConsoleOutputTest.pl
new file mode 100644
index 00000000000..bd398a5b9f7
--- /dev/null
+++ b/vespaclient/src/perl/test/Yahoo/Vespa/ConsoleOutputTest.pl
@@ -0,0 +1,47 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+use Test::More;
+
+BEGIN { use_ok( 'Yahoo::Vespa::ConsoleOutput' ); }
+require_ok( 'Yahoo::Vespa::ConsoleOutput' );
+
+ok( Yahoo::Vespa::ConsoleOutput::getVerbosity() == 3,
+ 'Default verbosity is 3' );
+ok( Yahoo::Vespa::ConsoleOutput::usingAnsiColors(),
+ 'Using ansi colors by default' );
+
+use TestUtils::VespaTest;
+
+printSpam "test\n";
+isOutput('', '', "No spam at level 3");
+
+printDebug "test\n";
+isOutput('', '', "No spam at level 3");
+
+printInfo "info test\n";
+isOutput("info test\n", '', "Info at level 3");
+
+printWarning "foo\n";
+isOutput("", "\e[93mfoo\e[0m\n", "Stderr output for warning");
+
+useColors(0);
+printWarning "foo\n";
+isOutput("", "foo\n", "Stderr output without ansi colors");
+
+Yahoo::Vespa::ConsoleOutput::setVerbosity(4);
+printSpam "test\n";
+isOutput('', '', "No spam at level 4");
+
+printDebug "test\n";
+isOutput("debug: test\n", '', "Debug at level 4");
+
+Yahoo::Vespa::ConsoleOutput::setVerbosity(5);
+printSpam "test\n";
+isOutput("spam: test\n", '', "Spam at level 5");
+
+printInfo "info test\n";
+isOutput("info: info test\n", '', "Type prefix at high verbosity");
+
+done_testing();
+
+exit(0);
diff --git a/vespaclient/src/perl/test/Yahoo/Vespa/HttpTest.pl b/vespaclient/src/perl/test/Yahoo/Vespa/HttpTest.pl
new file mode 100644
index 00000000000..88c2961e3a2
--- /dev/null
+++ b/vespaclient/src/perl/test/Yahoo/Vespa/HttpTest.pl
@@ -0,0 +1,140 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+#
+# Tests of the Http wrapper library..
+#
+# NOTE: Test server set up does not support content not ending in newline.
+#
+
+use strict;
+use Test::More;
+use Yahoo::Vespa::Mocks::HttpServerMock;
+
+BEGIN {
+ use_ok( 'Yahoo::Vespa::Http' );
+ *Http:: = *Yahoo::Vespa::Http::
+}
+require_ok( 'Yahoo::Vespa::Http' );
+
+my $httpTestServerPort = setupTestHttpServer();
+ok(defined $httpTestServerPort, "Test server set up");
+
+&testSimpleGet();
+&testAdvancedGet();
+&testFailingGet();
+&testSimplePost();
+&testJsonReturnInPost();
+
+done_testing();
+
+exit(0);
+
+sub filterRequest {
+ my ($request) = @_;
+ $request =~ s/\r//g;
+ $request =~ s/(Content-Length:\s*)\d+/$1##/g;
+ $request =~ s/(Host: localhost:)\d+/$1##/g;
+ $request =~ s/(?:Connection|TE|Client-[^:]+):[^\n]*\n//g;
+
+ return $request;
+}
+
+sub testSimpleGet {
+ my %r = Http::get('localhost', $httpTestServerPort, '/foo');
+ is( $r{'code'}, 200, "Get request code" );
+ is( $r{'status'}, 'OK', "Get request status" );
+
+ my $expected = <<EOS;
+HTTP/1.1 200 OK
+Content-Length: ##
+Content-Type: text/plain; charset=utf-8
+
+GET /foo HTTP/1.1
+Host: localhost:##
+User-Agent: Vespa-perl-script
+EOS
+ is( &filterRequest($r{'all'}), $expected, 'Get result' );
+}
+
+sub testAdvancedGet {
+ my @headers = ("X-Foo" => 'Bar');
+ my @uri_param = ("uricrap" => 'special=?&%value',
+ "other" => 'hmm');
+ my %r = Http::request('GET', 'localhost', $httpTestServerPort, '/foo',
+ \@uri_param, undef, \@headers);
+ is( $r{'code'}, 200, "Get request code" );
+ is( $r{'status'}, 'OK', "Get request status" );
+
+ my $expected = <<EOS;
+HTTP/1.1 200 OK
+Content-Length: ##
+Content-Type: text/plain; charset=utf-8
+
+GET /foo?uricrap=special%3D%3F%26%25value&other=hmm HTTP/1.1
+Host: localhost:##
+User-Agent: Vespa-perl-script
+X-Foo: Bar
+EOS
+ is( &filterRequest($r{'all'}), $expected, 'Get result' );
+}
+
+sub testFailingGet {
+ my @uri_param = ("code" => '501',
+ "status" => 'Works');
+ my %r = Http::request('GET', 'localhost', $httpTestServerPort, '/foo',
+ \@uri_param);
+ is( $r{'code'}, 501, "Get request code" );
+ is( $r{'status'}, 'Works', "Get request status" );
+
+ my $expected = <<EOS;
+HTTP/1.1 501 Works
+Content-Length: ##
+Content-Type: text/plain; charset=utf-8
+
+GET /foo?code=501&status=Works HTTP/1.1
+Host: localhost:##
+User-Agent: Vespa-perl-script
+EOS
+ is( &filterRequest($r{'all'}), $expected, 'Get result' );
+}
+
+sub testSimplePost {
+ my @uri_param = ("uricrap" => 'Rrr' );
+ my %r = Http::request('POST', 'localhost', $httpTestServerPort, '/foo',
+ \@uri_param, "Some content\n");
+ is( $r{'code'}, 200, "Get request code" );
+ is( $r{'status'}, 'OK', "Get request status" );
+
+ my $expected = <<EOS;
+HTTP/1.1 200 OK
+Content-Length: ##
+Content-Type: text/plain; charset=utf-8
+
+POST /foo?uricrap=Rrr HTTP/1.1
+Host: localhost:##
+User-Agent: Vespa-perl-script
+Content-Length: ##
+Content-Type: application/x-www-form-urlencoded
+
+Some content
+EOS
+ is( &filterRequest($r{'all'}), $expected, 'Get result' );
+}
+
+sub testJsonReturnInPost
+{
+ my @uri_param = ("contenttype" => 'application/json' );
+ my $json = "{ \"key\" : \"value\" }\n";
+ my %r = Http::request('POST', 'localhost', $httpTestServerPort, '/foo',
+ \@uri_param, $json);
+ is( $r{'code'}, 200, "Get request code" );
+ is( $r{'status'}, 'OK', "Get request status" );
+
+ my $expected = <<EOS;
+HTTP/1.1 200 OK
+Content-Length: ##
+Content-Type: application/json
+
+{ "key" : "value" }
+EOS
+ is( &filterRequest($r{'all'}), $expected, 'Get json result' );
+}
diff --git a/vespaclient/src/perl/test/Yahoo/Vespa/JsonTest.pl b/vespaclient/src/perl/test/Yahoo/Vespa/JsonTest.pl
new file mode 100644
index 00000000000..5da8ad0e270
--- /dev/null
+++ b/vespaclient/src/perl/test/Yahoo/Vespa/JsonTest.pl
@@ -0,0 +1,67 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+#
+# Tests of the Json wrapper library..
+#
+
+use Test::More;
+
+use strict;
+
+BEGIN {
+ use_ok( 'Yahoo::Vespa::Json' );
+ *Json:: = *Yahoo::Vespa::Json:: # Alias namespace
+}
+require_ok( 'Yahoo::Vespa::Json' );
+
+&testSimpleJson();
+
+done_testing();
+
+exit(0);
+
+sub testSimpleJson {
+ my $json = <<EOS;
+{
+ "foo" : "bar",
+ "map" : {
+ "abc" : "def",
+ "num" : 13.0
+ },
+ "array" : [
+ { "val1" : 3 },
+ { "val2" : 6 }
+ ]
+}
+EOS
+ my $parsed = Json::parse($json);
+ is( $parsed->{'foo'}, 'bar', 'json test 1' );
+ is( $parsed->{'map'}->{'abc'}, 'def', 'json test 2' );
+ is( $parsed->{'map'}->{'num'}, 13.0, 'json test 3' );
+ my $prettyPrint = <<EOS;
+{
+ "array" : [
+ {
+ "val1" : 3
+ },
+ {
+ "val2" : 6
+ }
+ ],
+ "map" : {
+ "num" : 13,
+ "abc" : "def"
+ },
+ "foo" : "bar"
+}
+EOS
+ is( Json::encode($parsed), $prettyPrint, 'simple json test - encode' );
+ my @keys = sort keys %{$parsed->{'map'}};
+ is( scalar @keys, 2, 'simple json test - map keys' );
+ is( $keys[0], 'abc', 'simple json test - map key 1' );
+ is( $keys[1], 'num', 'simple json test - map key 2' );
+
+ @keys = @{ $parsed->{'array'} };
+ is( scalar @keys, 2, 'simple json test - list keys' );
+ is( $keys[0]->{'val1'}, 3, 'simple json test - list key 1' );
+ is( $keys[1]->{'val2'}, 6, 'simple json test - list key 2' );
+}
diff --git a/vespaclient/src/perl/test/Yahoo/Vespa/Mocks/ClusterControllerMock.pm b/vespaclient/src/perl/test/Yahoo/Vespa/Mocks/ClusterControllerMock.pm
new file mode 100644
index 00000000000..661d8a5e051
--- /dev/null
+++ b/vespaclient/src/perl/test/Yahoo/Vespa/Mocks/ClusterControllerMock.pm
@@ -0,0 +1,258 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package Yahoo::Vespa::Mocks::ClusterControllerMock;
+
+use strict;
+use warnings;
+use URI::Escape;
+use Yahoo::Vespa::ConsoleOutput;
+use Yahoo::Vespa::Mocks::HttpClientMock;
+use Yahoo::Vespa::Utils;
+
+BEGIN {
+ use base 'Exporter';
+ our @EXPORT = qw(
+ );
+}
+
+our $forceInternalServerError = 0;
+
+# Register a handler in the Http Client mock
+registerHttpClientHandler(\&handleCCRequest);
+
+our $clusterListJson = <<EOS;
+{
+ "cluster" : {
+ "books" : {
+ "link" : "/cluster/v2/books"
+ },
+ "music" : {
+ "link" : "/cluster/v2/music"
+ }
+ }
+}
+EOS
+our $musicClusterJson = <<EOS;
+{
+ "state" : {
+ "generated" : {
+ "state" : "up",
+ "reason" : ""
+ }
+ },
+ "service" : {
+ "distributor" : {
+ "node" : {
+ "0" : {
+ "attributes" : { "hierarchical-group" : "top" },
+ "state" : {
+ "generated" : { "state" : "down", "reason" : "Setting it down" },
+ "unit" : { "state" : "up", "reason" : "Now reporting state U" },
+ "user" : { "state" : "down", "reason" : "Setting it down" }
+ }
+ },
+ "1" : {
+ "attributes" : { "hierarchical-group" : "top" },
+ "state" : {
+ "generated" : { "state" : "up", "reason" : "Setting it up" },
+ "unit" : { "state" : "up", "reason" : "Now reporting state U" },
+ "user" : { "state" : "up", "reason" : ""
+ }
+ }
+ }
+ }
+ },
+ "storage" : {
+ "node" : {
+ "0" : {
+ "attributes" : { "hierarchical-group" : "top" },
+ "state" : {
+ "generated" : { "state" : "retired", "reason" : "Stop using" },
+ "unit" : { "state" : "up", "reason" : "Now reporting state U" },
+ "user" : { "state" : "retired", "reason" : "Stop using" }
+ },
+ "partition" : {
+ "0" : {
+ "metrics" : {
+ "bucket-count" : 5,
+ "unique-document-count" : 10,
+ "unique-document-total-size" : 1000
+ }
+ }
+ }
+ },
+ "1" : {
+ "attributes" : { "hierarchical-group" : "top" },
+ "state" : {
+ "generated" : { "state" : "up", "reason" : "Setting it up" },
+ "unit" : { "state" : "up", "reason" : "Now reporting state U" },
+ "user" : { "state" : "up", "reason" : ""
+ }
+ },
+ "partition" : {
+ "0" : {
+ "metrics" : {
+ "bucket-count" : 50,
+ "unique-document-count" : 100,
+ "unique-document-total-size" : 10000
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+EOS
+our $booksClusterJson = <<EOS;
+{
+ "state" : {
+ "generated" : {
+ "state" : "up",
+ "reason" : ""
+ }
+ },
+ "service" : {
+ "distributor" : {
+ "node" : {
+ "0" : {
+ "attributes" : { "hierarchical-group" : "top.g1" },
+ "state" : {
+ "generated" : { "state" : "down", "reason" : "Setting it down" },
+ "unit" : { "state" : "up", "reason" : "Now reporting state U" },
+ "user" : { "state" : "down", "reason" : "Setting it down" }
+ }
+ },
+ "1" : {
+ "attributes" : { "hierarchical-group" : "top.g2" },
+ "state" : {
+ "generated" : { "state" : "up", "reason" : "Setting it up" },
+ "unit" : { "state" : "up", "reason" : "Now reporting state U" },
+ "user" : { "state" : "up", "reason" : ""
+ }
+ }
+ }
+ }
+ },
+ "storage" : {
+ "node" : {
+ "0" : {
+ "attributes" : { "hierarchical-group" : "top.g1" },
+ "state" : {
+ "generated" : { "state" : "down", "reason" : "Not seen" },
+ "unit" : { "state" : "down", "reason" : "Not in slobrok" },
+ "user" : { "state" : "down", "reason" : "default" }
+ }
+ },
+ "1" : {
+ "attributes" : { "hierarchical-group" : "top.g2" },
+ "state" : {
+ "generated" : { "state" : "up", "reason" : "Setting it up" },
+ "unit" : { "state" : "up", "reason" : "Now reporting state U" },
+ "user" : { "state" : "up", "reason" : ""
+ }
+ }
+ }
+ }
+ }
+ }
+}
+EOS
+
+return &init();
+
+sub init {
+ #print "Verifying that cluster list json is parsable.\n";
+ my $json = Json::parse($clusterListJson);
+ #print "Verifying that music json is parsable\n";
+ $json = Json::parse($musicClusterJson);
+ #print "Verifying that books json is parsable\n";
+ $json = Json::parse($booksClusterJson);
+ #print "All seems parsable.\n";
+ return 1;
+}
+
+sub setClusterDown {
+ $musicClusterJson =~ s/"up"/"down"/;
+ $musicClusterJson =~ s/""/"Not enough nodes up"/;
+ #print "Cluster state: $musicClusterJson\n";
+ #print "Verifying that music json is parsable\n";
+ my $json = Json::parse($musicClusterJson);
+}
+
+sub handleCCRequest { # (Type, Host, Port, Path, ParameterMap, Content, Headers)
+ my ($type, $host, $port, $path, $params, $content, $headers) = @_;
+ my %paramHash;
+ if (defined $params) {
+ %paramHash = @$params;
+ }
+ if ($forceInternalServerError) {
+ printDebug "Forcing internal server error response\n";
+ return (
+ 'code' => 500,
+ 'status' => 'Internal Server Error (forced)'
+ );
+ }
+ if ($path eq "/cluster/v2/") {
+ printDebug "Handling cluster list request\n";
+ return (
+ 'code' => 200,
+ 'status' => 'OK',
+ 'content' => $clusterListJson
+ );
+ }
+ if ($path eq "/cluster/v2/music/"
+ && (exists $paramHash{'recursive'}
+ && $paramHash{'recursive'} eq 'true'))
+ {
+ printDebug "Handling cluster music state request\n";
+ return (
+ 'code' => 200,
+ 'status' => 'OK',
+ 'content' => $musicClusterJson
+ );
+ }
+ if ($path eq "/cluster/v2/books/"
+ && (exists $paramHash{'recursive'}
+ && $paramHash{'recursive'} eq 'true'))
+ {
+ printDebug "Handling cluster books state request\n";
+ return (
+ 'code' => 200,
+ 'status' => 'OK',
+ 'content' => $booksClusterJson
+ );
+ }
+ if ($path =~ /^\/cluster\/v2\/(books|music)\/(storage|distributor)\/(\d+)$/)
+ {
+ my ($cluster, $service, $index) = ($1, $2, $3);
+ my $json = Json::parse($content);
+ my $state = $json->{'state'}->{'user'}->{'state'};
+ my $description = $json->{'state'}->{'user'}->{'reason'};
+ if (!defined $description && $state eq 'up') {
+ $description = "";
+ }
+ if ($state !~ /^(?:up|down|maintenance|retired)$/) {
+ return (
+ 'code' => 500,
+ 'status' => "Unknown state '$state' specified"
+ );
+ }
+ if (!defined $state || !defined $description) {
+ return (
+ 'code' => 500,
+ 'status' => "Invalid form data or failed parsing: '$content'"
+ );
+ }
+ printDebug "Handling set user state request $cluster/$service/$index";
+ return (
+ 'code' => 200,
+ 'status' => "Set user state for $cluster/$service/$index to "
+ . "'$state' with reason '$description'"
+ );
+ }
+ printDebug "Request to '$path' not matched. Params:\n";
+ foreach my $key (keys %paramHash) {
+ printDebug " $key => '$paramHash{$key}'\n";
+ }
+ return;
+}
diff --git a/vespaclient/src/perl/test/Yahoo/Vespa/Mocks/HttpClientMock.pm b/vespaclient/src/perl/test/Yahoo/Vespa/Mocks/HttpClientMock.pm
new file mode 100644
index 00000000000..22a15de28b7
--- /dev/null
+++ b/vespaclient/src/perl/test/Yahoo/Vespa/Mocks/HttpClientMock.pm
@@ -0,0 +1,55 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+#
+# Switched the backend implementation of the Vespa::Http library, such that
+# requests are sent here rather than onto the network. Register handlers here
+# to respond to requests.
+#
+# Handlers are called in sequence until one of them returns a defined result.
+# If none do, return a generic failure.
+#
+
+package Yahoo::Vespa::Mocks::HttpClientMock;
+
+use strict;
+use warnings;
+use Yahoo::Vespa::ConsoleOutput;
+use Yahoo::Vespa::Http;
+
+BEGIN { # - Define default exports for module
+ use base 'Exporter';
+ our @EXPORT = qw(
+ registerHttpClientHandler
+ );
+}
+
+my @HANDLERS;
+
+&initialize();
+
+return 1;
+
+#################### Default exported functions #############################
+
+sub registerHttpClientHandler { # (Handler)
+ push @HANDLERS, $_[0];
+}
+
+##################### Internal utility functions ##########################
+
+sub initialize { # ()
+ Yahoo::Vespa::Http::setHttpExecutor(\&clientMock);
+}
+sub clientMock { # (HttpRequest to forward) -> Response
+ foreach my $handler (@HANDLERS) {
+ my %result = &$handler(@_);
+ if (exists $result{'code'}) {
+ return %result;
+ }
+ }
+ return (
+ 'code' => 500,
+ 'status' => 'No client handler for given request',
+ 'content' => '',
+ 'all' => ''
+ );
+}
diff --git a/vespaclient/src/perl/test/Yahoo/Vespa/Mocks/HttpServerMock.pm b/vespaclient/src/perl/test/Yahoo/Vespa/Mocks/HttpServerMock.pm
new file mode 100644
index 00000000000..267a905b67d
--- /dev/null
+++ b/vespaclient/src/perl/test/Yahoo/Vespa/Mocks/HttpServerMock.pm
@@ -0,0 +1,156 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+#
+# A mock of an HTTP server, such that HTTP client library can be tested.
+#
+# Known limitations:
+# - Does line by line reading of TCP data, so the content part of the HTML
+# request has to end in a newline, otherwise, the server will block waiting
+# for more data.
+#
+# Default connection handler:
+# - If no special case, server returns request 200 OK, with the complete
+# client request as text/plain utf8 content.
+# - If request matches contenttype=\S+ (Typically due to setting a URI
+# parameter), the response will contain the content of the request with the
+# given content type set.
+# - If request matches code=\d+ (Typically due to setting a URI parameter),
+# the response will use that return code.
+# - If request matches status=\S+ (Typically due to setting a URI parameter),
+# the response will use that status line
+#
+
+package Yahoo::Vespa::Mocks::HttpServerMock;
+
+use strict;
+use warnings;
+use IO::Socket::IP;
+use URI::Escape;
+
+BEGIN { # - Set up exports for module
+ use base 'Exporter';
+ our @EXPORT = qw(
+ setupTestHttpServer
+ );
+}
+
+my $HTTP_TEST_SERVER;
+my $HTTP_TEST_SERVER_PORT;
+my $HTTP_TEST_SERVER_PID;
+my $CONNECTION_HANDLER = \&defaultConnectionHandler;
+
+END { # - Kill forked HTTP handler process on exit
+ if (defined $HTTP_TEST_SERVER_PID) {
+ kill(9, $HTTP_TEST_SERVER_PID);
+ }
+}
+
+return 1;
+
+####################### Default exported functions ############################
+
+sub setupTestHttpServer { # () -> HttpServerPort
+ my $portfile = "/tmp/vespaclient.$$.perl.httptestserverport";
+ unlink($portfile);
+ my $pid = fork();
+ if ($pid == 0) {
+ $HTTP_TEST_SERVER = IO::Socket::IP->new(
+ 'Proto' => 'tcp',
+ 'LocalPort' => 0,
+ 'Listen' => SOMAXCONN,
+ 'ReuseAddr' => 1,
+ );
+ # print "Started server listening to port " . $HTTP_TEST_SERVER->sockport()
+ # . "\n";
+ my $fh;
+ open ($fh, ">$portfile") or die "Failed to write port used to file.";
+ print $fh "<" . $HTTP_TEST_SERVER->sockport() . ">";
+ close $fh;
+ defined $HTTP_TEST_SERVER or die "Failed to set up test HTTP server";
+ while (1) {
+ &$CONNECTION_HANDLER();
+ }
+ exit(0);
+ } else {
+ $HTTP_TEST_SERVER_PID = $pid;
+ while (1) {
+ if (-e $portfile) {
+ my $port = `cat $portfile`;
+ chomp $port;
+ if (defined $port && $port =~ /\<(\d+)\>/) {
+ #print "Client using port $1\n";
+ $HTTP_TEST_SERVER_PORT = $1;
+ last;
+ }
+ }
+ sleep(0.01);
+ }
+ }
+ unlink($portfile);
+ return $HTTP_TEST_SERVER_PORT;
+}
+
+####################### Internal utility functions ############################
+
+sub defaultConnectionHandler {
+ my $client = $HTTP_TEST_SERVER->accept();
+ defined $client or die "No connection to accept?";
+ my $request;
+ my $line;
+ my $content_length = 0;
+ my $content_type;
+ while ($line = <$client>) {
+ if ($line =~ /^(.*?)\s$/) {
+ $line = $1;
+ }
+ if ($line =~ /Content-Length:\s(\d+)/) {
+ $content_length = $1;
+ }
+ if ($line =~ /contenttype=(\S+)/) {
+ $content_type = uri_unescape($1);
+ }
+ #print "Got line '$line'\n";
+ if ($line eq '') {
+ last;
+ }
+ $request .= $line . "\n";
+ }
+ if ($content_length > 0) {
+ $request .= "\n";
+ if (defined $content_type) {
+ $request = "";
+ }
+ my $read = 0;
+ while ($line = <$client>) {
+ $read += length $line;
+ if ($line =~ /^(.*?)\s$/) {
+ $line = $1;
+ }
+ $request .= $line;
+ if ($read >= $content_length) {
+ last;
+ }
+ }
+ }
+ # print "Got request '$request'.\n";
+ $request =~ s/\n/\r\n/g;
+ my $code = 200;
+ my $status = "OK";
+ if ($request =~ /code=(\d+)/) {
+ $code = $1;
+ }
+ if ($request =~ /status=([A-Za-z0-9]+)/) {
+ $status = $1;
+ }
+ my $response = "HTTP/1.1 $code $status\n";
+ if (defined $content_type) {
+ $response .= "Content-Type: $content_type\n";
+ } else {
+ $response .= "Content-Type: text/plain; charset=utf-8\n";
+ }
+ $response .= "Content-Length: " . (length $request) . "\n"
+ . "\n";
+ $response =~ s/\n/\r\n/g;
+ $response .= $request;
+ print $client $response;
+ close $client;
+}
diff --git a/vespaclient/src/perl/test/Yahoo/Vespa/Mocks/VespaModelMock.pm b/vespaclient/src/perl/test/Yahoo/Vespa/Mocks/VespaModelMock.pm
new file mode 100644
index 00000000000..78bce3f1e6c
--- /dev/null
+++ b/vespaclient/src/perl/test/Yahoo/Vespa/Mocks/VespaModelMock.pm
@@ -0,0 +1,96 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+package Yahoo::Vespa::Mocks::VespaModelMock;
+
+use strict;
+use warnings;
+use Yahoo::Vespa::VespaModel;
+
+Yahoo::Vespa::VespaModel::setModelRetrievalFunction(\&getModelConfig);
+
+our $defaultModelConfig = <<EOS;
+hosts[0].name "testhost.yahoo.com"
+hosts[0].services[0].name "container-clustercontroller"
+hosts[0].services[0].type "container-clustercontroller"
+hosts[0].services[0].configid "admin/cluster-controllers/0"
+hosts[0].services[0].clustertype ""
+hosts[0].services[0].clustername "cluster-controllers"
+hosts[0].services[0].index 0
+hosts[0].services[0].ports[0].number 19050
+hosts[0].services[0].ports[0].tags "state external query http"
+hosts[0].services[0].ports[1].number 19100
+hosts[0].services[0].ports[1].tags "external http"
+hosts[0].services[0].ports[2].number 19101
+hosts[0].services[0].ports[2].tags "messaging rpc"
+hosts[0].services[0].ports[3].number 19102
+hosts[0].services[0].ports[3].tags "admin rpc"
+hosts[0].services[1].name "distributor2"
+hosts[0].services[1].type "distributor"
+hosts[0].services[1].configid "music/distributor/0"
+hosts[0].services[1].clustertype "content"
+hosts[0].services[1].clustername "music"
+hosts[0].services[1].index 0
+hosts[0].services[1].ports[0].number 19131
+hosts[0].services[1].ports[0].tags "messaging"
+hosts[0].services[1].ports[1].number 19132
+hosts[0].services[1].ports[1].tags "status rpc"
+hosts[0].services[1].ports[2].number 19133
+hosts[0].services[1].ports[2].tags "status http"
+hosts[0].services[2].name "storagenode3"
+hosts[0].services[2].type "storagenode"
+hosts[0].services[2].configid "storage/storage/0"
+hosts[0].services[2].clustertype "content"
+hosts[0].services[2].clustername "books"
+hosts[0].services[2].index 0
+hosts[0].services[2].ports[0].number 19134
+hosts[0].services[2].ports[0].tags "messaging"
+hosts[0].services[2].ports[1].number 19135
+hosts[0].services[2].ports[1].tags "status rpc"
+hosts[0].services[2].ports[2].number 19136
+hosts[0].services[2].ports[2].tags "status http"
+hosts[1].name "other.host.yahoo.com"
+hosts[1].services[0].name "distributor2"
+hosts[1].services[0].type "distributor"
+hosts[1].services[0].configid "music/distributor/1"
+hosts[1].services[0].clustertype "content"
+hosts[1].services[0].clustername "music"
+hosts[1].services[0].index 1
+hosts[1].services[0].ports[0].number 19131
+hosts[1].services[0].ports[0].tags "messaging"
+hosts[1].services[0].ports[1].number 19132
+hosts[1].services[0].ports[1].tags "status rpc"
+hosts[1].services[0].ports[2].number 19133
+hosts[1].services[0].ports[2].tags "status http"
+hosts[1].services[1].name "storagenode3"
+hosts[1].services[1].type "storagenode"
+hosts[1].services[1].configid "storage/storage/1"
+hosts[1].services[1].clustertype "content"
+hosts[1].services[1].clustername "books"
+hosts[1].services[1].index 1
+hosts[1].services[1].ports[0].number 19134
+hosts[1].services[1].ports[0].tags "messaging"
+hosts[1].services[1].ports[1].number 19135
+hosts[1].services[1].ports[1].tags "status rpc"
+hosts[1].services[1].ports[2].number 19136
+hosts[1].services[1].ports[2].tags "status http"
+hosts[1].services[2].name "storagenode2"
+hosts[1].services[2].type "storagenode"
+hosts[1].services[2].configid "storage/storage/0"
+hosts[1].services[2].clustertype "content"
+hosts[1].services[2].clustername "music"
+hosts[1].services[2].index 0
+hosts[1].services[2].ports[0].number 19134
+hosts[1].services[2].ports[0].tags "messaging"
+hosts[1].services[2].ports[1].number 19135
+hosts[1].services[2].ports[1].tags "status rpc"
+hosts[1].services[2].ports[2].number 19136
+hosts[1].services[2].ports[2].tags "status http"
+
+EOS
+
+sub getModelConfig {
+ my @output = split(/\n/, $defaultModelConfig);
+ return @output;
+}
+
+1;
diff --git a/vespaclient/src/perl/test/Yahoo/Vespa/VespaModelTest.pl b/vespaclient/src/perl/test/Yahoo/Vespa/VespaModelTest.pl
new file mode 100644
index 00000000000..fdb6a85bb16
--- /dev/null
+++ b/vespaclient/src/perl/test/Yahoo/Vespa/VespaModelTest.pl
@@ -0,0 +1,63 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+use Test::More;
+use Yahoo::Vespa::Mocks::VespaModelMock;
+
+BEGIN {
+ use_ok( 'Yahoo::Vespa::VespaModel' );
+ *VespaModel:: = *Yahoo::Vespa::VespaModel:: ;
+}
+require_ok( 'Yahoo::Vespa::VespaModel' );
+
+&testGetSocketForService();
+&testVisitServices();
+
+done_testing();
+
+exit(0);
+
+sub testGetSocketForService {
+ my $sockets = VespaModel::getSocketForService(
+ type => 'container-clustercontroller', tag => 'state');
+ my ($host, $port) = ($$sockets[0]->{'host'}, $$sockets[0]->{'port'});
+ is( $host, 'testhost.yahoo.com', "Host for state API" );
+ is( $port, 19050, 'Port for state API' );
+ $sockets = VespaModel::getSocketForService(
+ type => 'container-clustercontroller', tag => 'admin');
+ ($host, $port) = ($$sockets[0]->{'host'}, $$sockets[0]->{'port'});
+ is( $host, 'testhost.yahoo.com', "Host for state API" );
+ is( $port, 19102, 'Port for state API' );
+ $sockets = VespaModel::getSocketForService(
+ type => 'container-clustercontroller', tag => 'http');
+ ($host, $port) = ($$sockets[0]->{'host'}, $$sockets[0]->{'port'});
+ is( $port, 19100, 'Port for state API' );
+
+ $sockets = VespaModel::getSocketForService(
+ type => 'distributor', index => 0);
+ ($host, $port) = ($$sockets[0]->{'host'}, $$sockets[0]->{'port'});
+ is( $host, 'testhost.yahoo.com', 'host for distributor 0' );
+}
+
+my @services;
+
+sub serviceCallback {
+ my ($info) = @_;
+ push @services, "Name($$info{'name'}) Type($$info{'type'}) "
+ . "Cluster($$info{'cluster'}) Host($$info{'host'}) "
+ . "Index($$info{'index'})";
+}
+
+sub testVisitServices {
+ @services = ();
+ VespaModel::visitServices(\&serviceCallback);
+ my $expected = <<EOS;
+Name(storagenode3) Type(storagenode) Cluster(books) Host(testhost.yahoo.com) Index(0)
+Name(storagenode3) Type(storagenode) Cluster(books) Host(other.host.yahoo.com) Index(1)
+Name(container-clustercontroller) Type(container-clustercontroller) Cluster(cluster-controllers) Host(testhost.yahoo.com) Index(0)
+Name(distributor2) Type(distributor) Cluster(music) Host(testhost.yahoo.com) Index(0)
+Name(distributor2) Type(distributor) Cluster(music) Host(other.host.yahoo.com) Index(1)
+Name(storagenode2) Type(storagenode) Cluster(music) Host(other.host.yahoo.com) Index(0)
+EOS
+ chomp $expected;
+ is ( join("\n", @services), $expected, "Services visited correctly" );
+}