Tag: PSGI

  • Perl debugger superpowers, part 2

    Perl debugger superpowers, part 2

    In March I wrote The Perl debug­ger can be your super­pow­er, intro­duc­ing the step debug­ger as a bet­ter way to debug your Perl code rather than lit­ter­ing your source with tem­po­rary print state­ments or log­ging. I use the debug­ger all the time, and I’ve real­ized that some more tech­niques are worth covering.

    Although I men­tioned a caveat when debug­ging web appli­ca­tions, our apps at work all adhere to the Perl Web Server Gateway Interface (PSGI) spec­i­fi­ca­tion and thus we can use tools like Test::WWW::Mechanize::PSGI or Plack::Test to run tests and debug­ging ses­sions in the same Perl process. (Mojolicious users can use some­thing like Test::Mojo for the same effect.)

    To demon­strate, let’s get start­ed with some­thing like this which tests that a giv­en route (/say-hello) returns a cer­tain JSON struc­ture ({"message": "Hello world!"}):

    #!/usr/bin/env perl
    
    use Test::Most;
    use Test::WWW::Mechanize::PSGI;
    use JSON::MaybeXS;
    use Local::MyApp; # name of app's main module
    
    my $mech = Test::WWW::Mechanize::PSGI->new(
        # a Dancer2 app, so to_app returns a PSGI coderef
        app => Local::MyApp->to_app(),
    );
    $mech->get_ok('/say-hello');
    lives_and {
        my $json = decode_json($mech->content);
        cmp_deeply( $json, {message => 'Hello world!'} );
    } 'message is Hello world!';
    
    done_testing;

    All very fine and well, but what hap­pens if that route starts return­ing a dif­fer­ent mes­sage or worse, invalid out­put that caus­es decode_json to fail? Eventually, you’ll rewrite the test in the script to out­put the offend­ing con­tent when some­thing goes wrong, but right now you want to suss out the root cause.

    Debuggers have the con­cept of break­points, which are flags that tell the debug­ger to stop at a cer­tain line of code and wait for instruc­tions. We can set them while run­ning the debug­ger with the b com­mand or con­tin­ue to a one-​time break­point with the c com­mand, or we can insert them into the code our­selves before run­ning it through the debug­ger in the first place.

    Add this line right after the lives_and { line:

    $DB::single = 1;

    This sim­u­lates hav­ing typed the s com­mand in the debug­ger at that line, stop­ping exe­cu­tion at that point. Run our test with per­l’s -d option, and then type c to con­tin­ue to that breakpoint:

    $ perl -d -Ilib t/test_psgi.t
    
    Loading DB routines from perl5db.pl version 1.60
    Editor support available.
    
    Enter h or 'h h' for help, or 'man perldebug' for more help.
    
    [Local::MyApp:7170] core @2021-07-06 07:33:22> Built config from files: /Users/mgardner/Projects/blog/myapp/config.yml /Users/mgardner/Projects/blog/myapp/environments/development.yml in (eval 310)[/Users/mgardner/.plenv/versions/5.34.0/lib/perl5/site_perl/5.34.0/Sub/Quote.pm:3] l. 910
    Test2::API::CODE(0x7ffabea39ee8)(/Users/mgardner/.plenv/versions/5.34.0/lib/perl5/site_perl/5.34.0/Test2/API.pm:71):
    71:	    INIT { eval 'END { test2_set_is_end() }; 1' or die $@ }
    
      DB<1> c
    
    [...]
    ok 1 - GET /say-hello
    main::CODE(0x7f8069caf2c8)(t/test_psgi.t:14):
    15:	    my $json = decode_json($mech->content);
    
      DB<1> 

    From here we can exam­ine vari­ables, set oth­er break­points, or even exe­cute arbi­trary lines of code. Let’s see what became of that HTTP GET request:

      DB<1> x $mech->content
    
    0  '{"error":"Undefined subroutine &Local::MyApp::build_frog called at lib/Local/MyApp.pm line 11.\\n"}'
    
      DB<2> 

    Aha, some­thing has returned some dif­fer­ent JSON indi­cat­ing an error. Let’s look at the lines around (1020) the offend­ing line (11):

      DB<2> f lib/Local/MyApp.pm
    
      DB<3> l 10-20
    
    10:	        my $method = 'build_frog';
    11:	        $method->();
    12 	    }
    13:	    catch ($e) {
    14:	        send_as JSON => {error => $e};
    15 	    }
    16:	    send_as JSON => {message => 'Hello world!'};
    17:	};
    18
    19 	sub build_frob {
    20:	    return;
    
      DB<4>

    Yep, a typo on line 11, and one that was­n’t caught at com­pile time since it’s gen­er­at­ed at runtime.

    Just to be sure (and to demon­strate some oth­er cool debug­ger fea­tures), let’s set anoth­er break­point while in the debug­ger and then exer­cise that route again. Then we’ll check that $method vari­able against the list of avail­able meth­ods in the Local::MyApp package.

      DB<4> b 11
    
      DB<5> $mech->get('/say-hello')
    
    [...]
    Local::MyApp::CODE(0x7f8066f2db60)(lib/Local/MyApp.pm:11):
    11:	        $method->();
    
      DB<<6>> x $method
    
    0  'build_frog'
    
      DB<<7>> m Local::MyApp
    any
    app
    body_parameters
    build_frob
    captures
    config
    content
    [...]
      DB<<8>>

    No doubt about it, that vari­able is being set incorrectly.

    Quit out of the debug­ger with the q com­mand, make the fix (we prob­a­bly want errors to give some­thing oth­er than an HTTP 200 OK while we’re at it), and re-​run the test:

    $ perl -Ilib t/test_psgi.t
    
    [Local::MyApp:8277] core @2021-07-06 07:48:36> Built config from files: /Users/mgardner/Projects/blog/myapp/config.yml /Users/mgardner/Projects/blog/myapp/environments/development.yml in (eval 309) l. 910
    Name "DB::single" used only once: possible typo at t/test_psgi.t line 13.
    [...]
    ok 1 - GET /say-hello
    ok 2 - message is Hello world!
    1..2

    Note that warn­ing about leav­ing $DB::single in there. While harm­less, it’s a good reminder to remove such lines from your code so that they don’t sur­prise you or your team­mates dur­ing future debug­ging sessions.

    And that’s it. Note that because we’re using PSGI, we were able to set break­points in our web app code itself and the debug­ger stopped there and enabled us to have a look around. And as you’ve seen, once you’re at a break­point you can switch to dif­fer­ent files, add/​remove more break­points, run arbi­trary code, and more. The perlde­bug doc­u­men­ta­tion page has all the details.

    Happy debug­ging! For your ref­er­ence, here’s the full app mod­ule and test script used in this article:

    MyApp.pm

    package Local::MyApp;
    use Dancer2;
    use Feature::Compat::Try;
    
    our $VERSION = '0.1';
    
    get '/say-hello' => sub {
        try {
            no strict 'refs';
            my $method = 'build_frob';
            $method->();
        }
        catch ($e) {
            status 'error';
            send_as JSON => {error => $e};
        }
        send_as JSON => {message => 'Hello world!'};
    };
    
    sub build_frob {
        return;
    }
    
    true;

    test_psgi.t

    #!/usr/bin/env perl
    
    use Test::Most;
    use Test::WWW::Mechanize::PSGI;
    use JSON::MaybeXS;
    use Local::MyApp; # name of your app's main module goes here
    
    my $mech = Test::WWW::Mechanize::PSGI->new(
        # a Dancer2 app, so to_app returns a PSGI coderef
        app => Local::MyApp->to_app(),
    );
    $mech->get_ok('/say-hello');
    lives_and {
        my $json = decode_json($mech->content);
        cmp_deeply( $json, {message => 'Hello world!'} );
    } 'message is Hello world!';
    
    done_testing;