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­geras a bet­ter way to debug your Perl code rather than lit­ter­ing your source with tem­po­rary printstate­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 caveatwhen 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::PSGIor Plack::Testto run tests and debug­ging ses­sions in the same Perl process. (Mojolicious users can use some­thing like Test::Mojofor 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 JSONstruc­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_jsonto 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 bcom­mand or con­tin­ue to a one-​time break­point with the ccom­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 scom­mand in the debug­ger at that line, stop­ping exe­cu­tion at that point. Run our test with per­l’s -doption, and then type cto 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 GETrequest:

  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 (10−20) 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 $methodvari­able against the list of avail­able meth­ods in the Local::MyApppackage.

  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 qcom­mand, make the fix (we prob­a­bly want errors to give some­thing oth­er than an HTTP 200 OKwhile 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­ingabout leav­ing $DB::singlein 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­bugdoc­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;

Discover more from The Phoenix Trap

Subscribe to get the latest posts sent to your email.

Mark Gardner Avatar

Hi, I’m Mark.

Hi, I’m Mark Gard­ner, and this is my personal blog. I show software developers how to level up by building production-ready things that work. Clear code, real projects, lessons learned.

Comments

5 responses to “Perl debugger superpowers, part 2”

  1. hikoto Avatar
    hikoto

    I found I can’t under­stand your last arti­cle. today I know this is because I even don’t know what is the debug­ger 🙁
    this first time I know perl have this func­tion, thanks.

    1. Mark Gardner Avatar
      1. hikoto Avatar
        hikoto

        thank you 😊

  2. jiyon16015 Avatar
    jiyon16015

    Great arti­cle as always. You do not know me. But you have helped me a lot in the past. So thank you.

    Btw, I think you should con­sid­er doing a debug­ger review of hdb some­time. I have used it and it’s very easy to use com­pared to some oth­er graph­i­cal debug­gers like ptkdb. Please con­sid­er doing a review of hdb. Thank you again.

    1. Mark Gardner Avatar

      I guess you mean Devel::hdb? I’m try­ing it out and it looks pret­ty neat. The one thing I miss is being able to exe­cute arbi­trary code dur­ing a break­point. It’s still worth a follow-​up arti­cle, though. Thanks!

Your post must include a link to this post.

How does this work?
Write a response on your own website and include a link to this post. Then submit your response’s URL here. Your response may appear here after moderation. To update or remove it, edit or delete your original post and submit the same URL again. Learn more about Webmentions.