1package Plack::Test::Suite;
2use strict;
3use warnings;
4use Digest::MD5;
5use File::ShareDir;
6use HTTP::Request;
7use HTTP::Request::Common;
8use Test::More;
9use Test::TCP;
10use Plack::Loader;
11use Plack::Middleware::Lint;
12use Plack::Util;
13use Plack::Request;
14use Try::Tiny;
15use Plack::LWPish;
16
17my $share_dir = try { File::ShareDir::dist_dir('Plack') } || 'share';
18
19$ENV{PLACK_TEST_SCRIPT_NAME} = '';
20
21# 0: test name
22# 1: request generator coderef.
23# 2: request handler
24# 3: test case for response
25our @TEST = (
26    [
27        'SCRIPT_NAME',
28        sub {
29            my $cb = shift;
30            my $res = $cb->(GET "http://127.0.0.1/");
31            is $res->content, "script_name=$ENV{PLACK_TEST_SCRIPT_NAME}";
32        },
33        sub {
34            my $env = shift;
35            return [ 200, ["Content-Type", "text/plain"], [ "script_name=$env->{SCRIPT_NAME}" ] ];
36        },
37    ],
38    [
39        'GET',
40        sub {
41            my $cb = shift;
42            my $res = $cb->(GET "http://127.0.0.1/?name=miyagawa");
43            is $res->code, 200;
44            is $res->message, 'OK';
45            is $res->header('content_type'), 'text/plain';
46            is $res->content, 'Hello, name=miyagawa';
47        },
48        sub {
49            my $env = shift;
50            return [
51                200,
52                [ 'Content-Type' => 'text/plain', ],
53                [ 'Hello, ' . $env->{QUERY_STRING} ],
54            ];
55        },
56    ],
57    [
58        'POST',
59        sub {
60            my $cb = shift;
61            my $res = $cb->(POST "http://127.0.0.1/", [name => 'tatsuhiko']);
62            is $res->code, 200;
63            is $res->message, 'OK';
64            is $res->header('Client-Content-Length'), 14;
65            is $res->header('Client-Content-Type'), 'application/x-www-form-urlencoded';
66            is $res->header('content_type'), 'text/plain';
67            is $res->content, 'Hello, name=tatsuhiko';
68        },
69        sub {
70            my $env = shift;
71            my $body;
72            $env->{'psgi.input'}->read($body, $env->{CONTENT_LENGTH});
73            return [
74                200,
75                [ 'Content-Type' => 'text/plain',
76                  'Client-Content-Length' => $env->{CONTENT_LENGTH},
77                  'Client-Content-Type' => $env->{CONTENT_TYPE},
78              ],
79                [ 'Hello, ' . $body ],
80            ];
81        },
82    ],
83    [
84        'big POST',
85        sub {
86            my $cb = shift;
87            my $chunk = "abcdefgh" x 12000;
88            my $req = HTTP::Request->new(POST => "http://127.0.0.1/");
89            $req->content_length(length $chunk);
90            $req->content_type('application/octet-stream');
91            $req->content($chunk);
92
93            my $res = $cb->($req);
94            is $res->code, 200;
95            is $res->message, 'OK';
96            is $res->header('Client-Content-Length'), length $chunk;
97            is length $res->content, length $chunk;
98            is Digest::MD5::md5_hex($res->content), Digest::MD5::md5_hex($chunk);
99        },
100        sub {
101            my $env = shift;
102            my $len = $env->{CONTENT_LENGTH};
103            my $body = '';
104            my $spin;
105            while ($len > 0) {
106                my $rc = $env->{'psgi.input'}->read($body, $env->{CONTENT_LENGTH}, length $body);
107                $len -= $rc;
108                last if $spin++ > 2000;
109            }
110            return [
111                200,
112                [ 'Content-Type' => 'text/plain',
113                  'Client-Content-Length' => $env->{CONTENT_LENGTH},
114                  'Client-Content-Type' => $env->{CONTENT_TYPE},
115              ],
116                [ $body ],
117            ];
118        },
119    ],
120    [
121        'psgi.url_scheme',
122        sub {
123            my $cb = shift;
124            my $res = $cb->(POST "http://127.0.0.1/");
125            is $res->code, 200;
126            is $res->message, 'OK';
127            is $res->header('content_type'), 'text/plain';
128            is $res->content, 'http';
129        },
130        sub {
131            my $env = $_[0];
132            return [
133                200,
134                [ 'Content-Type' => 'text/plain', ],
135                [ $env->{'psgi.url_scheme'} ],
136            ];
137        },
138    ],
139    [
140        'return glob',
141        sub {
142            my $cb  = shift;
143            my $res = $cb->(GET "http://127.0.0.1/");
144            is $res->code, 200;
145            is $res->message, 'OK';
146            is $res->header('content_type'), 'text/plain';
147            like $res->content, qr/^package /;
148            like $res->content, qr/END_MARK_FOR_TESTING$/;
149        },
150        sub {
151            my $env = shift;
152            open my $fh, '<', __FILE__ or die $!;
153            return [
154                200,
155                [ 'Content-Type' => 'text/plain', ],
156                $fh,
157            ];
158        },
159    ],
160    [
161        'filehandle',
162        sub {
163            my $cb  = shift;
164            my $res = $cb->(GET "http://127.0.0.1/foo.jpg");
165            is $res->code, 200;
166            is $res->message, 'OK';
167            is $res->header('content_type'), 'image/jpeg';
168            is length $res->content, 2898;
169        },
170        sub {
171            my $env = shift;
172            open my $fh, '<', "$share_dir/face.jpg";
173            return [
174                200,
175                [ 'Content-Type' => 'image/jpeg', 'Content-Length' => -s $fh ],
176                $fh
177            ];
178        },
179    ],
180    [
181        'bigger file',
182        sub {
183            my $cb  = shift;
184            my $res = $cb->(GET "http://127.0.0.1/baybridge.jpg");
185            is $res->code, 200;
186            is $res->message, 'OK';
187            is $res->header('content_type'), 'image/jpeg';
188            is length $res->content, 14750;
189            is Digest::MD5::md5_hex($res->content), '70546a79c7abb9c497ca91730a0686e4';
190        },
191        sub {
192            my $env = shift;
193            open my $fh, '<', "$share_dir/baybridge.jpg";
194            binmode $fh;
195            return [
196                200,
197                [ 'Content-Type' => 'image/jpeg', 'Content-Length' => -s $fh ],
198                $fh
199            ];
200        },
201    ],
202    [
203        'handle HTTP-Header',
204        sub {
205            my $cb  = shift;
206            my $res = $cb->(GET "http://127.0.0.1/foo/?dankogai=kogaidan", Foo => "Bar");
207            is $res->code, 200;
208            is $res->message, 'OK';
209            is $res->header('content_type'), 'text/plain';
210            is $res->content, 'Bar';
211        },
212        sub {
213            my $env = shift;
214            return [
215                200,
216                [ 'Content-Type' => 'text/plain', ],
217                [$env->{HTTP_FOO}],
218            ];
219        },
220    ],
221    [
222        'handle HTTP-Cookie',
223        sub {
224            my $cb  = shift;
225            my $res = $cb->(GET "http://127.0.0.1/foo/?dankogai=kogaidan", Cookie => "foo");
226            is $res->code, 200;
227            is $res->message, 'OK';
228            is $res->header('content_type'), 'text/plain';
229            is $res->content, 'foo';
230        },
231        sub {
232            my $env = shift;
233            return [
234                200,
235                [ 'Content-Type' => 'text/plain', ],
236                [$env->{HTTP_COOKIE}],
237            ];
238        },
239    ],
240    [
241        'validate env',
242        sub {
243            my $cb  = shift;
244            my $res = $cb->(GET "http://127.0.0.1/foo/?dankogai=kogaidan");
245            is $res->code, 200;
246            is $res->message, 'OK';
247            is $res->header('content_type'), 'text/plain';
248            is $res->content, join("\n",
249                'REQUEST_METHOD:GET',
250                "SCRIPT_NAME:$ENV{PLACK_TEST_SCRIPT_NAME}",
251                'PATH_INFO:/foo/',
252                'QUERY_STRING:dankogai=kogaidan',
253                'SERVER_NAME:127.0.0.1',
254                "SERVER_PORT:" . $res->request->uri->port,
255            )."\n";
256        },
257        sub {
258            my $env = shift;
259            my $body;
260            $body .= $_ . ':' . $env->{$_} . "\n" for qw/REQUEST_METHOD SCRIPT_NAME PATH_INFO QUERY_STRING SERVER_NAME SERVER_PORT/;
261            return [
262                200,
263                [ 'Content-Type' => 'text/plain', ],
264                [$body],
265            ];
266        },
267    ],
268    [
269        '% encoding in PATH_INFO',
270        sub {
271            my $cb  = shift;
272            my $res = $cb->(GET "http://127.0.0.1/foo/bar%2cbaz");
273            is $res->content, "/foo/bar,baz", "PATH_INFO should be decoded per RFC 3875";
274        },
275        sub {
276            my $env = shift;
277            return [
278                200,
279                [ 'Content-Type' => 'text/plain', ],
280                [ $env->{PATH_INFO} ],
281            ];
282        },
283    ],
284    [
285        '% double encoding in PATH_INFO',
286        sub {
287            my $cb  = shift;
288            my $res = $cb->(GET "http://127.0.0.1/foo/bar%252cbaz");
289            is $res->content, "/foo/bar%2cbaz", "PATH_INFO should be decoded only once, per RFC 3875";
290        },
291        sub {
292            my $env = shift;
293            return [
294                200,
295                [ 'Content-Type' => 'text/plain', ],
296                [ $env->{PATH_INFO} ],
297            ];
298        },
299    ],
300    [
301        '% encoding in PATH_INFO (outside of URI characters)',
302        sub {
303            my $cb  = shift;
304            my $res = $cb->(GET "http://127.0.0.1/foo%E3%81%82");
305            is $res->content, "/foo\x{e3}\x{81}\x{82}";
306        },
307        sub {
308            my $env = shift;
309            return [
310                200,
311                [ 'Content-Type' => 'text/plain', ],
312                [ $env->{PATH_INFO} ],
313            ];
314        },
315    ],
316    [
317        'SERVER_PROTOCOL is required',
318        sub {
319            my $cb  = shift;
320            my $res = $cb->(GET "http://127.0.0.1/foo/?dankogai=kogaidan");
321            is $res->code, 200;
322            is $res->message, 'OK';
323            is $res->header('content_type'), 'text/plain';
324            like $res->content, qr{^HTTP/1\.[01]$};
325        },
326        sub {
327            my $env = shift;
328            return [
329                200,
330                [ 'Content-Type' => 'text/plain', ],
331                [$env->{SERVER_PROTOCOL}],
332            ];
333        },
334    ],
335    [
336        'SCRIPT_NAME should not be undef',
337        sub {
338            my $cb  = shift;
339            my $res = $cb->(GET "http://127.0.0.1/foo/?dankogai=kogaidan");
340            is $res->content, 1;
341        },
342        sub {
343            my $env = shift;
344            my $cont = defined $env->{'SCRIPT_NAME'};
345            return [
346                200,
347                [ 'Content-Type' => 'text/plain', ],
348                [$cont],
349            ];
350        },
351    ],
352    [
353        'call close after read IO::Handle-like',
354        sub {
355            my $cb  = shift;
356            my $res = $cb->(GET "http://127.0.0.1/call_close");
357            is($res->content, '1234');
358        },
359        sub {
360            my $env = shift;
361            {
362                our $closed = -1;
363                sub CalledClose::new { $closed = 0; my $i=0; bless \$i, 'CalledClose' }
364                sub CalledClose::getline {
365                    my $self = shift;
366                    return $$self++ < 4 ? $$self : undef;
367                }
368                sub CalledClose::close { ::ok(1, 'closed') if defined &::ok }
369            }
370            return [
371                200,
372                [ 'Content-Type' => 'text/plain', ],
373                CalledClose->new(),
374            ];
375        },
376    ],
377    [
378        'has errors',
379        sub {
380            my $cb  = shift;
381            my $res = $cb->(GET "http://127.0.0.1/has_errors");
382            is $res->content, 1;
383        },
384        sub {
385            my $env = shift;
386            my $err = $env->{'psgi.errors'};
387            my $has_errors = defined $err;
388            return [
389                200,
390                [ 'Content-Type' => 'text/plain', ],
391                [$has_errors]
392            ];
393        },
394    ],
395    [
396        'status line',
397        sub {
398            my $cb  = shift;
399            my $res = $cb->(GET "http://127.0.0.1/foo/?dankogai=kogaidan");
400            is($res->status_line, '200 OK');
401        },
402        sub {
403            my $env = shift;
404            return [
405                200,
406                [ 'Content-Type' => 'text/plain', ],
407                [1]
408            ];
409        },
410    ],
411    [
412        'Do not crash when the app dies',
413        sub {
414            my $cb  = shift;
415            my $res = $cb->(GET "http://127.0.0.1/");
416            is $res->code, 500;
417            is $res->message, 'Internal Server Error';
418        },
419        sub {
420            my $env = shift;
421            open my $io, '>', \my $error;
422            $env->{'psgi.errors'} = $io;
423            die "Throwing an exception from app handler. Server shouldn't crash.";
424        },
425    ],
426    [
427        'multi headers (request)',
428        sub {
429            my $cb  = shift;
430            my $req = HTTP::Request->new(
431                GET => "http://127.0.0.1/",
432            );
433            $req->push_header(Foo => "bar");
434            $req->push_header(Foo => "baz");
435            my $res = $cb->($req);
436            like($res->content, qr/^bar,\s*baz$/);
437        },
438        sub {
439            my $env = shift;
440            return [
441                200,
442                [ 'Content-Type' => 'text/plain', ],
443                [ $env->{HTTP_FOO} ]
444            ];
445        },
446    ],
447    [
448        'multi headers (response)',
449        sub {
450            my $cb  = shift;
451            my $res = $cb->(HTTP::Request->new(GET => "http://127.0.0.1/"));
452            my $foo = $res->header('X-Foo');
453            like $foo, qr/foo,\s*bar,\s*baz/;
454        },
455        sub {
456            my $env = shift;
457            return [
458                200,
459                [ 'Content-Type' => 'text/plain', 'X-Foo', 'foo', 'X-Foo', 'bar, baz' ],
460                [ 'hi' ]
461            ];
462        },
463    ],
464    [
465        'Do not set $env->{COOKIE}',
466        sub {
467            my $cb  = shift;
468            my $req = HTTP::Request->new(
469                GET => "http://127.0.0.1/",
470            );
471            $req->push_header(Cookie => "foo=bar");
472            my $res = $cb->($req);
473            is($res->header('X-Cookie'), 0);
474            is $res->content, 'foo=bar';
475        },
476        sub {
477            my $env = shift;
478            return [
479                200,
480                [ 'Content-Type' => 'text/plain', 'X-Cookie' => $env->{COOKIE} ? 1 : 0 ],
481                [ $env->{HTTP_COOKIE} ]
482            ];
483        },
484    ],
485    [
486        'no entity headers on 304',
487        sub {
488            my $cb  = shift;
489            my $res = $cb->(GET "http://127.0.0.1/");
490            is $res->code, 304;
491            is $res->message, 'Not Modified';
492            is $res->content, '';
493            ok ! defined $res->header('content_type'), "No Content-Type";
494            ok ! defined $res->header('content_length'), "No Content-Length";
495            ok ! defined $res->header('transfer_encoding'), "No Transfer-Encoding";
496        },
497        sub {
498            my $env = shift;
499            return [ 304, [], [] ];
500        },
501    ],
502    [
503        'REQUEST_URI is set',
504        sub {
505            my $cb  = shift;
506            my $res = $cb->(GET "http://127.0.0.1/foo/bar%20baz%73?x=a");
507            is $res->content, $ENV{PLACK_TEST_SCRIPT_NAME} . "/foo/bar%20baz%73?x=a";
508        },
509        sub {
510            my $env = shift;
511            return [ 200, [ 'Content-Type' => 'text/plain' ], [ $env->{REQUEST_URI} ] ];
512        },
513    ],
514    [
515        'filehandle with path()',
516        sub {
517            my $cb  = shift;
518            my $res = $cb->(GET "http://127.0.0.1/foo.jpg");
519            is $res->code, 200;
520            is $res->message, 'OK';
521            is $res->header('content_type'), 'image/jpeg';
522            is length $res->content, 2898;
523        },
524        sub {
525            my $env = shift;
526            open my $fh, '<', "$share_dir/face.jpg";
527            Plack::Util::set_io_path($fh, "$share_dir/face.jpg");
528            return [
529                200,
530                [ 'Content-Type' => 'image/jpeg', 'Content-Length' => -s $fh ],
531                $fh
532            ];
533        },
534    ],
535    [
536        'a big header value > 128 bytes',
537        sub {
538            my $cb  = shift;
539            my $req = GET "http://127.0.0.1/";
540            my $v = ("abcdefgh" x 16);
541            $req->header('X-Foo' => $v);
542            my $res = $cb->($req);
543            is $res->code, 200;
544            is $res->message, 'OK';
545            is $res->content, $v;
546        },
547        sub {
548            my $env = shift;
549            return [
550                200,
551                [ 'Content-Type' => 'text/plain' ],
552                [ $env->{HTTP_X_FOO} ],
553            ];
554        },
555    ],
556    [
557        'coderef res',
558        sub {
559            my $cb = shift;
560            my $res = $cb->(GET "http://127.0.0.1/?name=miyagawa");
561            return if $res->code == 501;
562
563            is $res->code, 200;
564            is $res->message, 'OK';
565            is $res->header('content_type'), 'text/plain';
566            is $res->content, 'Hello, name=miyagawa';
567        },
568        sub {
569            my $env = shift;
570            $env->{'psgi.streaming'} or return [ 501, ['Content-Type','text/plain'], [] ];
571            return sub {
572                my $respond = shift;
573                $respond->([
574                    200,
575                    [ 'Content-Type' => 'text/plain', ],
576                    [ 'Hello, ' . $env->{QUERY_STRING} ],
577                ]);
578            }
579        },
580    ],
581    [
582        'coderef streaming',
583        sub {
584            my $cb = shift;
585            my $res = $cb->(GET "http://127.0.0.1/?name=miyagawa");
586            return if $res->code == 501;
587
588            is $res->code, 200;
589            is $res->message, 'OK';
590            is $res->header('content_type'), 'text/plain';
591            is $res->content, 'Hello, name=miyagawa';
592        },
593        sub {
594            my $env = shift;
595            $env->{'psgi.streaming'} or return [ 501, ['Content-Type','text/plain'], [] ];
596
597            return sub {
598                my $respond = shift;
599
600                my $writer = $respond->([
601                    200,
602                    [ 'Content-Type' => 'text/plain', ],
603                ]);
604
605                $writer->write("Hello, ");
606                $writer->write($env->{QUERY_STRING});
607                $writer->close();
608            }
609        },
610    ],
611    [
612        'CRLF output and FCGI parse bug',
613        sub {
614            my $cb = shift;
615            my $res = $cb->(GET "http://127.0.0.1/");
616
617            is $res->header("Foo"), undef;
618            is $res->content, "Foo: Bar\r\n\r\nHello World";
619        },
620        sub {
621            return [ 200, [ "Content-Type", "text/plain" ], [ "Foo: Bar\r\n\r\nHello World" ] ];
622        },
623    ],
624    [
625        'newlines',
626        sub {
627            my $cb = shift;
628            my $res = $cb->(GET "http://127.0.0.1/");
629            is length($res->content), 7;
630        },
631        sub {
632            return [ 200, [ "Content-Type", "text/plain" ], [ "Bar\nBaz" ] ];
633        },
634    ],
635    [
636        'test 404',
637        sub {
638            my $cb = shift;
639            my $res = $cb->(GET "http://127.0.0.1/");
640            is $res->code, 404;
641            is $res->message, 'Not Found';
642            is $res->content, 'Not Found';
643        },
644        sub {
645            return [ 404, [ "Content-Type", "text/plain" ], [ "Not Found" ] ];
646        },
647    ],
648    [
649        'request->input seekable',
650        sub {
651            my $cb = shift;
652            my $req = HTTP::Request->new(POST => "http://127.0.0.1/");
653            $req->content("body");
654            $req->content_type('text/plain');
655            $req->content_length(4);
656            my $res = $cb->($req);
657            is $res->content, 'body';
658        },
659        sub {
660            my $req = Plack::Request->new(shift);
661            return [ 200, [ "Content-Type", "text/plain" ], [ $req->content ] ];
662        },
663    ],
664    [
665        'request->content on GET',
666        sub {
667            my $cb = shift;
668            my $res = $cb->(GET "http://127.0.0.1/");
669            ok $res->is_success;
670        },
671        sub {
672            my $req = Plack::Request->new(shift);
673            $req->content;
674            return [ 200, [ "Content-Type", "text/plain" ], [ "OK" ] ];
675        },
676    ],
677    [
678        'handle Authorization header',
679        sub {
680            my $cb  = shift;
681            SKIP: {
682                skip "Authorization header is unsupported under CGI", 4 if ($ENV{PLACK_TEST_HANDLER} || "") eq "CGI";
683
684                {
685                    my $req = HTTP::Request->new(
686                        GET => "http://127.0.0.1/",
687                    );
688                    $req->push_header(Authorization => 'Basic XXXX');
689                    my $res = $cb->($req);
690                    is $res->header('X-AUTHORIZATION'), 1;
691                    is $res->content, 'Basic XXXX';
692                };
693
694                {
695                    my $req = HTTP::Request->new(
696                        GET => "http://127.0.0.1/",
697                    );
698                    my $res = $cb->($req);
699                    is $res->header('X-AUTHORIZATION'), 0;
700                    is $res->content, 'no_auth';
701                };
702            };
703        },
704        sub {
705            my $env = shift;
706            return [
707                200,
708                [ 'Content-Type' => 'text/plain', 'X-AUTHORIZATION' => exists($env->{HTTP_AUTHORIZATION}) ? 1 : 0 ],
709                [ $env->{HTTP_AUTHORIZATION} || 'no_auth' ],
710            ];
711        },
712    ],
713    [
714        'repeated slashes',
715        sub {
716            my $cb = shift;
717            my $res = $cb->(GET "http://127.0.0.1//foo///bar/baz");
718            is $res->code, 200;
719            is $res->message, 'OK';
720            is $res->header('content_type'), 'text/plain';
721            is $res->content, '//foo///bar/baz';
722        },
723        sub {
724            my $env = shift;
725            return [
726                200,
727                [ 'Content-Type' => 'text/plain', ],
728                [ $env->{PATH_INFO} ],
729            ];
730        },
731    ],
732);
733
734sub runtests {
735    my($class, $runner) = @_;
736    for my $test (@TEST) {
737        $runner->(@$test);
738    }
739}
740
741sub run_server_tests {
742    my($class, $server, $server_port, $http_port, %args) = @_;
743
744    if (ref $server ne 'CODE') {
745        my $server_class = $server;
746        $server = sub {
747            my($port, $app) = @_;
748            my $server = Plack::Loader->load($server_class, port => $port, host => "127.0.0.1", %args);
749            $app = Plack::Middleware::Lint->wrap($app);
750            $server->run($app);
751        }
752    }
753
754    test_tcp(
755        client => sub {
756            my $port = shift;
757            my $ua = Plack::LWPish->new( no_proxy => [qw/127.0.0.1/] );
758            for my $i (0..$#TEST) {
759                my $test = $TEST[$i];
760                note $test->[0];
761                my $cb = sub {
762                    my $req = shift;
763                    $req->uri->port($http_port || $port);
764                    $req->uri->path(($ENV{PLACK_TEST_SCRIPT_NAME}||"") . $req->uri->path);
765                    $req->header('X-Plack-Test' => $i);
766                    return $ua->request($req);
767                };
768
769                $test->[1]->($cb);
770            }
771        },
772        server => sub {
773            my $port = shift;
774            my $app  = $class->test_app_handler;
775            $server->($port, $app);
776            exit(0); # for Test::TCP
777        },
778        port => $server_port,
779    );
780}
781
782sub test_app_handler {
783    return sub {
784        my $env = shift;
785        $TEST[$env->{HTTP_X_PLACK_TEST}][2]->($env);
786    };
787}
788
7891;
790__END__
791
792=head1 NAME
793
794Plack::Test::Suite - Test suite for Plack handlers
795
796=head1 SYNOPSIS
797
798  use Test::More;
799  use Plack::Test::Suite;
800  Plack::Test::Suite->run_server_tests('Your::Handler');
801  done_testing;
802
803=head1 DESCRIPTION
804
805Plack::Test::Suite is a test suite to test a new PSGI server
806implementation. It automatically loads a new handler environment and
807uses LWP to send HTTP requests to the local server to make sure your
808handler implements the PSGI specification correctly.
809
810Note that the handler name doesn't include the C<Plack::Handler::>
811prefix, i.e. if you have a new Plack handler Plack::Handler::Foo, your
812test script would look like:
813
814  Plack::Test::Suite->run_server_tests('Foo');
815
816Developers writing Plack applications should look at C<Plack::Test> for testing,
817as subclassing C<Plack::Handler> is for developing server implementations.
818
819=head1 AUTHOR
820
821Tokuhiro Matsuno
822
823Tatsuhiko Miyagawa
824
825Kazuho Oku
826
827=cut
828
829END_MARK_FOR_TESTING
830