A curious case of a directory separator related test failure in Dancer2

After getting side-tracked, trying to fix a hard-coded path in a test file, and finally succeeding, I thought for moment that cpanm Dancer2 would now work without problems. But, alas, it was not to be. Only, this time, the failure was a real WTF moment:

# Failed test 'Default caller' # at t/classes/Dancer2-Core-Role-HasLocation/with.t line 31. # got: 't/classes/Dancer2-Core-Role-HasLocation/with.t' # expected: 't\classes\Dancer2-Core-Role-HasLocation\with.t' # Looks like you failed 1 test of 11.

Look carefully, and you may notice that the expected string has the correct directory separators.

Why is that?

After all, the test file uses File::Spec->catfile:

    my $path = File::Spec->catfile(qw<
        t classes Dancer2-Core-Role-HasLocation with.t
    >);

    is( $app->caller, $path, 'Default caller' );

For some reason, $app->caller is returning Unix style directory separators. Finding the reason took some time. caller in Dancer2::Core::Role::HasLocation looks innocuous enough. Further more, there are numerous calls to File::Spec methods from Dancer2::Core::Role::HasLocation::_build_location.

It looks like we must go in to Dancer2::FileUtils.

Looking at that module, we see:

sub path {
    my @parts = @_;
    my $path  = File::Spec->catfile(@parts);

    return normalize_path($path);
}

So, we use catfile to build the path … But, then, what is this normalize_path?

Ya da ya da … That wasn’t leading anywhere.

So, I did:

C:\…\Dancer2-0.155001> prove -vb t\classes\Dancer2-Core-Role-HasLocation\with.t
t\classes\Dancer2-Core-Role-HasLocation\with.t ..
1..11
# Defaults:
ok 1 - An object of class 'App' isa 'App'
ok 2 - App->can(...)
ok 3 - App->can('_build_location')
ok 4 - App consumes Dancer2::Core::Role::HasLocation
ok 5 - Default caller
# With lib/ and bin/:
ok 6 - An object of class 'App' isa 'App'
ok 7 - Got correct location with lib/ and bin/
# With .dancer file:
ok 8 - An object of class 'App' isa 'App'
ok 9 - Got correct location with .dancer file
# blib/ ignored:
ok 10 - An object of class 'App' isa 'App'
ok 11 - blib/ dir is ignored
ok
All tests successful.
Files=1, Tests=11,  1 wallclock secs ( 0.14 usr +  0.01 sys =  0.16 CPU)
Result: PASS

Looks perfect.

What could be the problem?

Let’s try Build test:

t/classes/Dancer2-Core-Role-Handler/with.t ............ ok
t/classes/Dancer2-Core-Role-HasLocation/with.t ........ 1/11
#   Failed test 'Default caller'
#   at t/classes/Dancer2-Core-Role-HasLocation/with.t line 31.
#          got: 't/classes/Dancer2-Core-Role-HasLocation/with.t'
#     expected: 't\classes\Dancer2-Core-Role-HasLocation\with.t'
# Looks like you failed 1 test of 11.

After a lot of squinting, I believe the problem lies in Module::Build::Base::rscan_dir. Everything else takes great care to use File::Spec whereas rscandir just takes whatever File::Find gives it.

File::Find has always returned Unix style paths:

C:\&hellip\Dancer2-0.155001> perl -MFile::Find -E "find(sub { say $File::Find::name }, 't')"
…
t/classes/Dancer2-Core-Role-Engine/with.t
t/classes/Dancer2-Core-Role-Handler
t/classes/Dancer2-Core-Role-Handler/with.t
t/classes/Dancer2-Core-Role-HasLocation
t/classes/Dancer2-Core-Role-HasLocation/with.t
t/classes/Dancer2-Core-Role-HasLocation/FakeDancerDir
…

The real fix should go into Module::Build. In terms of defensive programming, however, the test in Dancer2 can be fixed by using File::Spec->canonpath:

    my $path = File::Spec->catfile(qw<
        t classes Dancer2-Core-Role-HasLocation with.t
    >);

    is(
        File::Spec->canonpath($app->caller),
        $path,
        'Default caller'
    );

PS: See also Dancer2 issue #679.