github.com/olivere/camlistore@v0.0.0-20140121221811-1b7ac2da0199/server/tester/bs-test.pl (about)

     1  #!/usr/bin/perl
     2  #
     3  # Test script to run against a Camli blobserver to test its compliance
     4  # with the spec.
     5  
     6  use strict;
     7  use Getopt::Long;
     8  use LWP;
     9  use Test::More;
    10  
    11  my $user;
    12  my $password;
    13  my $implopt; 
    14  GetOptions("user" => \$user,
    15             "password" => \$password,
    16             "impl=s" => \$implopt,
    17      ) or usage();
    18  
    19  my $impl;
    20  my %args = (user => $user, password => $password);
    21  if ($implopt eq "go") {
    22      $impl = Impl::Go->new(%args);
    23  } elsif ($implopt eq "appengine") {
    24      $impl = Impl::AppEngine->new(%args);
    25  } else {
    26      die "The --impl flag must be 'go' or 'appengine'.\n";
    27  }
    28  
    29  ok($impl->start, "Server started");
    30  
    31  $impl->verify_no_blobs;  # also tests some of enumerate
    32  $impl->test_stat_and_upload;
    33  $impl->test_upload_corrupt_blob;  # blobref digest doesn't match
    34  
    35  # TODO: test multiple uploads in a batch
    36  # TODO: test uploads in serial (using each response's next uploadUrl)
    37  # TODO: test enumerate boundaries
    38  # TODO: interrupt a POST upload in the middle; verify no straggler on
    39  #       disk in subsequent GET
    40  # ....
    41  # test auth works on bogus password?  (auth still undefined)
    42  # TODO: test stat with both GET and POST (currently just POST)
    43  
    44  done_testing();
    45  
    46  sub usage {
    47      die "Usage: bs-test.pl [--user= --password=] --impl={go,appengine}\n";
    48  }
    49  
    50  package Impl;
    51  use HTTP::Request::Common;
    52  use LWP::UserAgent;
    53  use JSON::Any;
    54  use Test::More;
    55  use Digest::SHA1 qw(sha1_hex);
    56  use URI::URL ();
    57  use Data::Dumper;
    58  
    59  sub new {
    60      my ($class, %args) = @_;
    61      return bless \%args, $class;
    62  }
    63  
    64  sub post {
    65      my ($self, $path, $form) = @_;
    66      $path ||= "";
    67      $form ||= {};
    68      return POST($self->path($path),
    69                  "Authorization" => "Basic dGVzdDp0ZXN0", # test:test
    70                  Content => $form);
    71  }
    72  
    73  sub upload_request {
    74      my ($self, $upload_url, $blobref_to_blob_map) = @_;
    75      my @content;
    76      my $n = 0;
    77      foreach my $key (sort keys %$blobref_to_blob_map) {
    78          $n++;
    79          # TODO: the App Engine client refused to work unless the Content-Type
    80          # is set.  This should be clarified in the docs (MUST?) and update the
    81          # test suite and Go server accordingly (to fail if not present).
    82          push @content, $key => [
    83              undef, "filename$n",
    84              "Content-Type" => "application/octet-stream",
    85              Content => $blobref_to_blob_map->{$key},
    86          ];
    87      }
    88  
    89      return POST($upload_url,
    90                  "Content_Type" => 'form-data',
    91                  "Authorization" => "Basic dGVzdDp0ZXN0", # test:test
    92                  Content => \@content);
    93  }
    94  
    95  sub get {
    96      my ($self, $path, $form) = @_;
    97      $path ||= "";
    98      $form ||= {};
    99      return GET($self->path($path),
   100                 "Authorization" => "Basic dGVzdDp0ZXN0", # test:test
   101                 %$form);
   102  }
   103  
   104  sub head {
   105      my ($self, $path, $form) = @_;
   106      $path ||= "";
   107      $form ||= {};
   108      return HEAD($self->path($path),
   109                 "Authorization" => "Basic dGVzdDp0ZXN0", # test:test
   110                 %$form);
   111  }
   112  
   113  sub ua {
   114      my $self = shift;
   115      return ($self->{_ua} ||= LWP::UserAgent->new(agent => "camli/blobserver-tester"));
   116  }
   117  
   118  sub root {
   119      my $self= shift;
   120      return $self->{root} or die "No 'root' for $self";
   121  }
   122  
   123  sub path {
   124      my $self = shift;
   125      my $path = shift || "";
   126      return $self->root . $path;
   127  }
   128  
   129  sub get_json {
   130      my ($self, $req, $msg, $opts) = @_;
   131      $opts ||= {};
   132  
   133      my $res = $self->ua->request($req);
   134      ok(defined($res), "got response for HTTP request '$msg'");
   135  
   136      if ($res->code =~ m!^30[123]$! && $opts->{follow_redirect}) {
   137          my $location = $res->header("Location");
   138          if ($res->code == "303") {
   139              $req->method("GET");
   140          }
   141          my $new_uri = URI::URL->new($location, $req->uri)->abs;
   142          diag("Old URI was " . $req->uri);
   143          diag("New is " . $new_uri);
   144          diag("Redirecting HTTP request '$msg' to $location ($new_uri)");
   145          $req->uri($new_uri);
   146          $res = $self->ua->request($req);
   147          ok(defined($res), "got redirected response for HTTP request '$msg'");
   148      }
   149  
   150      ok($res->is_success, "successful response for HTTP request '$msg'")
   151          or diag("Status was: " . $res->status_line);
   152      my $json = JSON::Any->jsonToObj($res->content);
   153      is("HASH", ref($json), "JSON parsed for HTTP request '$msg'")
   154          or BAIL_OUT("expected JSON response");
   155      return $json;
   156  }
   157  
   158  sub get_upload_json {
   159      my ($self, $req) = @_;
   160      return $self->get_json($req, "upload", { follow_redirect => 1 })
   161  }
   162  
   163  sub verify_no_blobs {
   164      my $self = shift;
   165      my $req = $self->get("/camli/enumerate-blobs", {
   166          "after" => "",
   167          "limit" => 10,
   168      });
   169      my $json = $self->get_json($req, "enumerate empty blobs");
   170      ok(defined($json->{'blobs'}), "enumerate has a 'blobs' key");
   171      is("ARRAY", ref($json->{'blobs'}), "enumerate's blobs key is an array");
   172      is(0, scalar @{$json->{'blobs'}}, "no blobs on server");
   173  }
   174  
   175  sub test_stat_and_upload {
   176      my $self = shift;
   177      my ($req, $res);
   178  
   179      my $blob = "This is a line.\r\nWith mixed newlines\rFoo\nAnd binary\0data.\0\n\r.";
   180      my $blobref = "sha1-" . sha1_hex($blob);
   181  
   182      # Bogus method.
   183      $req = $self->head("/camli/stat", {
   184          "camliversion" => 1,
   185          "blob1" => $blobref,
   186      });
   187      $res = $self->ua->request($req);
   188      ok(!$res->is_success, "returns failure for HEAD on /camli/stat");
   189  
   190      # Correct method, but missing camliVersion.
   191      $req = $self->post("/camli/stat", {
   192          "blob1" => $blobref,
   193      });
   194      $res = $self->ua->request($req);
   195      ok(!$res->is_success, "returns failure for missing camliVersion param on stat");
   196  
   197      # Valid pre-upload
   198      $req = $self->post("/camli/stat", {
   199          "camliversion" => 1,
   200          "blob1" => $blobref,
   201      });
   202      my $jres = $self->get_json($req, "valid stat");
   203      diag("stat response: " . Dumper($jres));
   204      ok($jres, "valid stat JSON response");
   205      for my $f (qw(stat maxUploadSize uploadUrl uploadUrlExpirationSeconds)) {
   206          ok(defined($jres->{$f}), "required field '$f' present");
   207      }
   208      is(scalar(keys %$jres), 4, "Exactly 4 JSON keys returned");
   209      my $statList = $jres->{stat};
   210      is(ref($statList), "ARRAY", "stat is an array");
   211      is(scalar(@$statList), 0, "server doesn't have this blob yet.");
   212      like($jres->{uploadUrlExpirationSeconds}, qr/^\d+$/, "uploadUrlExpirationSeconds is numeric");
   213      my $upload_url = URI::URL->new($jres->{uploadUrl}, $self->root)->abs;
   214      ok($upload_url, "valid uploadUrl");
   215      # TODO: test & clarify in spec: are relative URLs allowed in uploadUrl?
   216      # App Engine seems to do it already, and makes it easier, so probably
   217      # best to clarify that they're relative.
   218  
   219      # Do the actual upload
   220      my $upreq = $self->upload_request($upload_url, {
   221          $blobref => $blob,
   222      });
   223      diag("upload request: " . $upreq->as_string);
   224      my $upres = $self->get_upload_json($upreq);
   225      ok($upres, "Upload was success");
   226      print STDERR "# upload response: ", Dumper($upres);
   227  
   228      for my $f (qw(uploadUrlExpirationSeconds uploadUrl maxUploadSize received)) {
   229          ok(defined($upres->{$f}), "required upload response field '$f' present");
   230      }
   231      is(scalar(keys %$upres), 4, "Exactly 4 JSON keys returned");
   232  
   233      like($upres->{uploadUrlExpirationSeconds}, qr/^\d+$/, "uploadUrlExpirationSeconds is numeric");
   234      is(ref($upres->{received}), "ARRAY", "'received' is an array")
   235          or BAIL_OUT();
   236      my $got = $upres->{received};
   237      is(scalar(@$got), 1, "got one file");
   238      is($got->[0]{blobRef}, $blobref, "received[0] 'blobRef' matches");
   239      is($got->[0]{size}, length($blob), "received[0] 'size' matches");
   240  
   241      # TODO: do a get request, verify that we get it back.
   242  }
   243  
   244  sub test_upload_corrupt_blob {
   245      my $self = shift;
   246      my ($req, $res);
   247  
   248      my $blob = "A blob, pre-corruption.";
   249      my $blobref = "sha1-" . sha1_hex($blob);
   250      $blob .= "OIEWUROIEWURLKJDSLKj CORRUPT";
   251  
   252      $req = $self->post("/camli/stat", {
   253          "camliversion" => 1,
   254          "blob1" => $blobref,
   255      });
   256      my $jres = $self->get_json($req, "valid stat");
   257      my $upload_url = URI::URL->new($jres->{uploadUrl}, $self->root)->abs;
   258      # TODO: test & clarify in spec: are relative URLs allowed in uploadUrl?
   259      # App Engine seems to do it already, and makes it easier, so probably
   260      # best to clarify that they're relative.
   261  
   262      # Do the actual upload
   263      my $upreq = $self->upload_request($upload_url, {
   264          $blobref => $blob,
   265      });
   266      diag("corrupt upload request: " . $upreq->as_string);
   267      my $upres = $self->get_upload_json($upreq);
   268      my $got = $upres->{received};
   269      is(ref($got), "ARRAY", "corrupt upload returned a 'received' array");
   270      is(scalar(@$got), 0, "didn't get any files (it was corrupt)");
   271  }
   272  
   273  package Impl::Go;
   274  use base 'Impl';
   275  use FindBin;
   276  use LWP::UserAgent;
   277  use HTTP::Request;
   278  use Fcntl;
   279  use File::Temp ();
   280  
   281  sub start {
   282      my $self = shift;
   283  
   284      $self->{_tmpdir_obj} = File::Temp->newdir();
   285      my $tmpdir = $self->{_tmpdir_obj}->dirname;
   286  
   287      die "Failed to create temporary directory." unless -d $tmpdir;
   288  
   289      system("$FindBin::Bin/../../build.pl", "server/go/blobserver")
   290          and die "Failed to build Go blobserver.";
   291  
   292      my $bindir = "$FindBin::Bin/../go/blobserver/";
   293      my $binary = "$bindir/blobserver";
   294  
   295      chdir($bindir) or die "filed to chdir to $bindir: $!";
   296      system("make") and die "failed to run make in $bindir";
   297  
   298      my ($port_rd, $port_wr, $exit_rd, $exit_wr);
   299      my $flags;
   300      pipe $port_rd, $port_wr;
   301      pipe $exit_rd, $exit_wr;
   302  
   303      $flags = fcntl($port_wr, F_GETFD, 0);
   304      fcntl($port_wr, F_SETFD, $flags & ~FD_CLOEXEC);
   305      $flags = fcntl($exit_rd, F_GETFD, 0);
   306      fcntl($exit_rd, F_SETFD, $flags & ~FD_CLOEXEC);
   307  
   308      $ENV{TESTING_PORT_WRITE_FD} = fileno($port_wr);
   309      $ENV{TESTING_CONTROL_READ_FD} = fileno($exit_rd);
   310      $ENV{CAMLI_PASSWORD} = "test";
   311  
   312      die "Binary $binary doesn't exist\n" unless -x $binary;
   313  
   314      my $pid = fork;
   315      die "Failed to fork" unless defined($pid);
   316      if ($pid == 0) {
   317          # child
   318          my @args = ($binary, "-listen=:0", "-root=$tmpdir");
   319          print STDERR "# Running: [@args]\n";
   320          exec @args;
   321          die "failed to exec: $!\n";
   322      }
   323      close($exit_rd);  # child owns this side
   324      close($port_wr);  # child owns this side
   325  
   326      print "Waiting for Go server to start...\n";
   327      my $line = <$port_rd>;
   328      close($port_rd);
   329  
   330      # Parse the port line out
   331      chomp $line;
   332      # print "Got port line: $line\n";
   333      die "Failed to start, no port info." unless $line =~ /:(\d+)$/;
   334      $self->{port} = $1;
   335      $self->{root} = "http://localhost:$self->{port}";
   336      print STDERR "# Running on $self->{root} ...\n";
   337  
   338      # Keep a reference to this to write "EXIT\n" to in order
   339      # to cleanly shutdown the child camlistored process.
   340      # If we close it, the child also dies, though.
   341      $self->{_exit_wr} = $exit_wr;
   342      return 1;
   343  }
   344  
   345  sub DESTROY {
   346      my $self = shift;
   347      syswrite($self->{_exit_wr}, "EXIT\n");
   348  }
   349  
   350  package Impl::AppEngine;
   351  use base 'Impl';
   352  use IO::Socket::INET;
   353  use Time::HiRes ();
   354  
   355  sub start {
   356      my $self = shift;
   357  
   358      my $dev_appserver = `which dev_appserver.py`;
   359      chomp $dev_appserver;
   360      unless ($dev_appserver && -x $dev_appserver) {
   361          $dev_appserver = "$ENV{HOME}/sdk/google_appengine/dev_appserver.py";
   362          unless (-x $dev_appserver) {
   363              die "No dev_appserver.py in \$PATH nor in \$HOME/sdk/google_appengine/dev_appserver.py\n";
   364          }
   365      }
   366  
   367      $self->{_tempdir_blobstore_obj} = File::Temp->newdir();
   368      $self->{_tempdir_datastore_obj} = File::Temp->newdir();
   369      my $datapath = $self->{_tempdir_blobstore_obj}->dirname . "/datastore-file";
   370      my $blobdir = $self->{_tempdir_datastore_obj}->dirname;
   371  
   372      my $port;
   373      while (1) {
   374          $port = int(rand(30000) + 1024);
   375          my $sock = IO::Socket::INET->new(Listen    => 5,
   376                                           LocalAddr => '127.0.0.1',
   377                                           LocalPort => $port,
   378                                           ReuseAddr => 1,
   379                                           Proto     => 'tcp');
   380          if ($sock) {
   381              last;
   382          }
   383      }
   384      $self->{port} = $port;
   385      $self->{root} = "http://localhost:$self->{port}";
   386  
   387      my $pid = fork;
   388      die "Failed to fork" unless defined($pid);
   389      if ($pid == 0) {
   390          my $appdir = "$FindBin::Bin/../appengine/blobserver";
   391  
   392          # child
   393          my @args = ($dev_appserver,
   394                      "--clear_datastore",  # kinda redundant as we made a temp dir
   395                      "--datastore_path=$datapath",
   396                      "--blobstore_path=$blobdir",
   397                      "--port=$port",
   398                      $appdir);
   399          print STDERR "# Running: [@args]\n";
   400          exec @args;
   401          die "failed to exec: $!\n";
   402      }
   403      $self->{pid} = $pid;
   404  
   405      my $last_print = 0;
   406      for (1..15) {
   407          my $now = time();
   408          if ($now != $last_print) {
   409              print STDERR "# Waiting for appengine app to start...\n";
   410              $last_print = $now;
   411          }
   412          my $res = $self->ua->request($self->get("/"));
   413          if ($res && $res->is_success) {
   414              print STDERR "# Up.";
   415              last;
   416          }
   417          Time::HiRes::sleep(0.1);
   418      }
   419      return 1;
   420  }
   421  
   422  sub DESTROY {
   423      my $self = shift;
   424      kill 3, $self->{pid} if $self->{pid};
   425  }
   426  
   427  1;
   428  
   429  
   430