diff options
author | Jon Bratseth <bratseth@yahoo-inc.com> | 2016-06-15 23:09:44 +0200 |
---|---|---|
committer | Jon Bratseth <bratseth@yahoo-inc.com> | 2016-06-15 23:09:44 +0200 |
commit | 72231250ed81e10d66bfe70701e64fa5fe50f712 (patch) | |
tree | 2728bba1131a6f6e5bdf95afec7d7ff9358dac50 /vespaclient/src/perl/test |
Publish
Diffstat (limited to 'vespaclient/src/perl/test')
17 files changed, 1857 insertions, 0 deletions
diff --git a/vespaclient/src/perl/test/Generic/UseTest.pl b/vespaclient/src/perl/test/Generic/UseTest.pl new file mode 100644 index 00000000000..d2c051d395a --- /dev/null +++ b/vespaclient/src/perl/test/Generic/UseTest.pl @@ -0,0 +1,34 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +# +# That that all perl files use strict and warnings +# + +use Test::More; +use TestUtils::VespaTest; + +use strict; +use warnings; + +my @dirs = ( + '../bin', + '../lib', + 'Yahoo/Vespa/Mocks' +); + +my $checkdirs = join(' ', @dirs); + +my @files = `find $checkdirs -name \\*.pm -or -name \\*.pl`; +chomp @files; + +printTest "Checking " . (scalar @files) . " files for includes.\n"; + +foreach my $file (@files) { + ok( system("cat $file | grep 'use strict;' >/dev/null") == 0, + "$file use strict" ); + ok( system("cat $file | grep 'use warnings;' >/dev/null") == 0, + "$file use warnings" ); +} + +done_testing(); + +exit(0); diff --git a/vespaclient/src/perl/test/TestUtils/OutputCapturer.pm b/vespaclient/src/perl/test/TestUtils/OutputCapturer.pm new file mode 100644 index 00000000000..cb36807999e --- /dev/null +++ b/vespaclient/src/perl/test/TestUtils/OutputCapturer.pm @@ -0,0 +1,112 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +package TestUtils::OutputCapturer; + +use Test::More; +use Yahoo::Vespa::ConsoleOutput; + +BEGIN { + use base 'Exporter'; + our @EXPORT = qw( + getOutput + isOutput + matchesOutput + ); +} + +Yahoo::Vespa::ConsoleOutput::setTerminalWidth(79); + +our ($stdout, $stderr); +my $USE_COLORS = 1; + +&openStreams(); + +END { + &closeStreams(); +} + +return 1; + +sub useColors { + $USE_COLORS = $_[0]; + &closeStreams(); + &openStreams(); +} + +sub isOutput { # (stdout, stderr, test) + my ($expected_cout, $expected_cerr, $test) = @_; + my ($cout, $cerr) = &getOutput(); + &diff($expected_cout, $cout); + ok ($cout eq $expected_cout, $test . " - stdout"); + &diff($expected_cerr, $cerr); + ok ($cerr eq $expected_cerr, $test . " - stderr"); +} + +sub matchesOutput { # (stdout_pattern, stderr_pattern, test) + my ($cout_pat, $cerr_pat, $test) = @_; + my ($cout, $cerr) = &getOutput(); + if ($cout !~ $cout_pat) { + diag("Output did not match standard out pattern:\n/$cout_pat/:\n$cout"); + } + ok ($cout =~ $cout_pat, $test . " - stdout"); + if ($cerr !~ $cerr_pat) { + diag("Stderr output did not match standard err pattern:\n" + . "/$cerr_pat/:\n$cerr"); + } + ok ($cerr =~ $cerr_pat, $test . " - stdout"); +} + +sub getOutput { + my $cout = &getStdOut(); + my $cerr = &getStdErr(); + &closeStreams(); + &openStreams(); + return ($cout, $cerr); +} + +sub openStreams { + open ($stdout, ">/tmp/vespaclient.perltest.stdout.log") + or die "Failed to create tmp file for stdout"; + open ($stderr, ">/tmp/vespaclient.perltest.stderr.log") + or die "Failed to create tmp file for stdout"; + Yahoo::Vespa::ConsoleOutput::initialize($stdout, $stderr, $USE_COLORS); +} + +sub closeStreams { + close $stdout; + close $stderr; + system("rm /tmp/vespaclient.perltest.stdout.log"); + system("rm /tmp/vespaclient.perltest.stderr.log"); +} + +sub getStdOut { + my $data = `cat /tmp/vespaclient.perltest.stdout.log`; + if (!defined $data) { $data = ''; } + return $data; +} + +sub getStdErr { + my $data = `cat /tmp/vespaclient.perltest.stderr.log`; + if (!defined $data) { $data = ''; } + return $data; +} + +sub diff { + my ($expected, $actual) = @_; + if ($expected eq $actual) { return; } + &writeToFile("/tmp/vespaclient.perltest.expected", $expected); + &writeToFile("/tmp/vespaclient.perltest.actual", $actual); + print "Output differs. Diff:\n"; + system("diff -u /tmp/vespaclient.perltest.expected " + . "/tmp/vespaclient.perltest.actual"); + system("rm -f /tmp/vespaclient.perltest.expected"); + system("rm -f /tmp/vespaclient.perltest.actual"); +} + +sub writeToFile { + my ($file, $data) = @_; + my $fh; + open ($fh, ">$file") or die "Failed to open temp file for writing."; + print $fh $data; + close $fh; +} diff --git a/vespaclient/src/perl/test/TestUtils/VespaTest.pm b/vespaclient/src/perl/test/TestUtils/VespaTest.pm new file mode 100644 index 00000000000..5df153e5938 --- /dev/null +++ b/vespaclient/src/perl/test/TestUtils/VespaTest.pm @@ -0,0 +1,92 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +package TestUtils::VespaTest; + +use Test::More; +use TestUtils::OutputCapturer; +use Yahoo::Vespa::Utils; + +BEGIN { + use base 'Exporter'; + our @EXPORT = qw( + isOutput + matchesOutput + setApplication + assertRun + assertRunMatches + printTest + useColors + setLocalHost + ); +} + +my $APPLICATION; + +&initialize(); + +return 1; + +sub initialize { + Yahoo::Vespa::Utils::initializeUnitTest( + 'testhost.yahoo.com', \&mockedExitHandler); +} + +sub setLocalHost { + my ($host) = @_; + Yahoo::Vespa::Utils::initializeUnitTest( + $host, \&mockedExitHandler); +} + +sub useColors { + TestUtils::OutputCapturer::useColors(@_); +} + +sub mockedExitHandler { + my ($exitcode) = @_; + die "Application exited with exitcode $exitcode."; +} + +sub setApplication { + my ($main_func) = @_; + $APPLICATION = $main_func; +} + +sub assertRun { + my ($testname, $argstring, + $expected_exitcode, $expected_stdout, $expected_stderr) = @_; + my $exitcode = &run($argstring); + is( $exitcode, $expected_exitcode, "$testname - exitcode" ); + # print OutputCapturer::getStdOut(); + isOutput($expected_stdout, $expected_stderr, $testname); +} + +sub assertRunMatches { + my ($testname, $argstring, + $expected_exitcode, $expected_stdout, $expected_stderr) = @_; + my $exitcode = &run($argstring); + is( $exitcode, $expected_exitcode, "$testname - exitcode" ); + # print OutputCapturer::getStdOut(); + matchesOutput($expected_stdout, $expected_stderr, $testname); +} + +sub run { + my ($argstring) = @_; + my @args = split(/\s+/, $argstring); + eval { + Yahoo::Vespa::ArgParser::initialize(); + &$APPLICATION(\@args); + }; + my $exitcode = 0; + if ($@) { + if ($@ =~ /Application exited with exitcode (\d+)\./) { + $exitcode = 1; + } else { + print "Unknown die signal '" . $@ . "'\n"; + } + } + return $exitcode; +} + +sub printTest { + print "Test: ", @_; +} 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" ); +} diff --git a/vespaclient/src/perl/test/testrunner.pl b/vespaclient/src/perl/test/testrunner.pl new file mode 100644 index 00000000000..c5307671b1a --- /dev/null +++ b/vespaclient/src/perl/test/testrunner.pl @@ -0,0 +1,110 @@ +#!/usr/bin/perl -w +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +# +# Searches around in test dir to find test binaries and run them. Sadly these +# seem to return exit code 0 on some failures for unknown reasons. To counter +# that the testrunner grabs the output of the test and triggers test to fail if +# it finds unexpected data in the output. +# +# Unit tests should mostly not write as this will clutter report, but if they +# want to write some status they have to write it so it does not trigger +# failure here. Use printTest in VespaTest suite to prefix all test output to +# something we match here. +# + +use strict; +use warnings; + +$| = 1; +my @files = `find . -name \*Test.pl`; +chomp @files; + +my $tempdir = `mktemp -d /tmp/mockup-vespahome-XXXXXX`; +chomp $tempdir; +$ENV{'VESPA_HOME'} = $tempdir . "/"; +mkdir "${tempdir}/libexec"; +mkdir "${tempdir}/libexec/vespa" or die "Cannot mkdir ${tempdir}/libexec/vespa\n"; +`touch ${tempdir}/libexec/vespa/common-env.sh`; + +my $pat; +if (exists $ENV{'TEST_SUBSET'}) { + $pat = $ENV{'TEST_SUBSET'}; +} + +my $failure_pattern = qr/(?:Tests were run but no plan was declared and done_testing\(\) was not seen)/; +my $accepted_pattern = qr/^(?:\s*|\d+\.\.\d+|ok\s+\d+\s+-\s+.*|Test: .*|.*spam: .*)$/; + +my $failures = 0; +foreach my $file (@files) { + $file =~ /^(?:\.\/)?(.*)\.pl$/ or die "Strange file name '$file'."; + my $test = $1; + if (!defined $pat || $test =~ /$pat/) { + print "\nRunning test suite $test.\n\n"; + my ($code, $result) = captureCommand("PERLLIB=../lib perl -w $file"); + my @data = split(/\n/, $result); + if ($code != 0) { + ++$failures; + print "Test binary returned with non-null exitcode. Failure.\n"; + } elsif (&matchesFailurePattern(\@data)) { + ++$failures; + } elsif (¬MatchesSuccessPattern(\@data)) { + ++$failures; + } + } else { + # print "Skipping test suite '$test' not matching '$pat'.\n"; + } +} + +if ($failures > 0) { + print "\n\n$failures test suites failed.\n"; + exit(1); +} else { + print "\n\nAll tests succeeded.\n"; +} + +`rm -rv ${tempdir}`; + +exit(0); + +sub matchesFailurePattern { # (LineArrayRef) + my ($data) = @_; + foreach my $line (@$data) { + if ($line =~ $failure_pattern) { + print "Line '$line' indicates failure. Failing test suite.\n"; + return 1; + } + } + return 0; +} + +sub notMatchesSuccessPattern { # (LineArrayRef) + my ($data) = @_; + foreach my $line (@$data) { + if ($line !~ $accepted_pattern) { + print "Suspicious line '$line'.\n"; + print "Failing test due to line suspected to indicate failure.\n" + . "(Use printTest to print debug data during test to have it " + . "not been marked suspected.\n"; + return 1; + } + } + return 0; +} + +# Run a given command, giving exitcode and output back, but let command write +# directly to stdout/stderr. (Useful for long running commands or commands that +# may stall, such that you can see where it got into trouble) +sub captureCommand { # (Cmd) -> (ExitCode, Output) + my ($cmd) = @_; + my ($fh, $line); + my $data; + open ($fh, "$cmd 2>&1 |") or die "Failed to run '$cmd'."; + while ($line = <$fh>) { + print $line; + $data .= $line; + } + close $fh; + my $exitcode = $?; + return ($exitcode >> 8, $data); +} |