#!/usr/bin/env perl use strict; use warnings; =head1 NAME 01-QMake-Project.t - test for QMake::Project =cut use FindBin; use lib "$FindBin::Bin/../../.."; use QMake::Project; use English qw(-no_match_vars); use File::Spec::Functions; use File::Which; use File::chdir; use Readonly; use Test::Exception; use Test::More; use Test::Warn; Readonly my $TESTDATA => catfile( $FindBin::Bin, 'test_projects' ); Readonly my $QMAKE => find_qmake( ); Readonly my $ERROR_RE => qr/^QMake::Project:/; sub run_qmake { my ($args) = @_; if (!$args) { $args = ""; } my $out = qx( $QMAKE $args 2>&1 ); is( $?, 0, 'qmake ok' ) || diag( $out ); ok( -f( 'Makefile' ), 'makefile was created' ); return; } # Basic test of a typical application .pro file sub test_typical { my ($proj) = @_; my $dir = "$TESTDATA/01-typical"; local $CWD = $dir; run_qmake( ); if ($proj) { $proj->set_makefile( 'Makefile' ); } else { $proj = QMake::Project->new( 'Makefile' ); } my $initial_count = $proj->{ _qmake_count }; is( $proj->values( 'TARGET' ), 'myapp' ); is( $proj->values( 'SOURCES' ), 'main.cpp' ); is( canonpath($proj->values( 'PWD' )), canonpath($dir) ); is( $proj->{ _qmake_count } - $initial_count, 3 ); return; } # Test that the default set TARGET still remains (even though we # used our own, differently-named .pro file) sub test_default_target { my ($proj) = @_; my $dir = "$TESTDATA/02-default-target"; local $CWD = $dir; run_qmake( ); # Check that we don't get messed up by any of make's environment # variables, which might be set if we are e.g. running via `make check' local $ENV{ MAKEFLAGS } = '-w -j3 -k'; local $ENV{ MFLAGS } = $ENV{ MAKEFLAGS }; local $ENV{ MAKELEVEL } = 1; if ($proj) { $proj->set_makefile( 'Makefile' ); } else { $proj = QMake::Project->new( 'Makefile' ); } my $initial_count = $proj->{ _qmake_count }; is( $proj->values( 'TARGET' ), '02-default-target' ); is( $proj->{ _qmake_count } - $initial_count, 1 ); return; } # Test retrieving list values sub test_list { my ($proj) = @_; my $dir = "$TESTDATA/03-list"; # Test being in a directory other than the project directory { local $CWD = $dir; run_qmake( ); } if ($proj) { $proj->set_makefile( "$dir/Makefile" ); } else { $proj = QMake::Project->new( "$dir/Makefile" ); } # Order of QT is undefined my %expected_qt = map { $_ => 1 } qw(core gui network xmlpatterns); my $initial_count = $proj->{ _qmake_count }; ok( $expected_qt{ $proj->values( 'QT' ) } ); is_deeply( [ sort $proj->values( 'QT' ) ], [ sort keys %expected_qt ] ); is( $proj->{ _qmake_count } - $initial_count, 2 ); return; } # Test some delayed evaluation sub test_delayed { my ($proj) = @_; my $dir = "$TESTDATA/03-list"; local $CWD = $dir; # Basic sanity check when qmake has extra args. run_qmake( '-spec default -nocache' ); if ($proj) { $proj->set_makefile( 'Makefile' ); } else { $proj = QMake::Project->new( 'Makefile' ); } my $initial_count = $proj->{ _qmake_count }; my $pro_file_pwd = $proj->values( '_PRO_FILE_PWD_' ); my $uses_gui = $proj->test( 'contains(QT,gui)' ); # Shouldn't have run qmake yet ... is( $proj->{ _qmake_count } - $initial_count, 0 ); is( canonpath( $pro_file_pwd ), canonpath( $dir ) ); is( $uses_gui, 1 ); # Should have run just once due to the above is( $proj->{ _qmake_count } - $initial_count, 1 ); my @qt = $proj->values( 'QT' ); # And again, due to list is( $proj->{ _qmake_count } - $initial_count, 2 ); # Order of QT is undefined is_deeply( [sort @qt], [ sort qw(core gui network xmlpatterns) ] ); return; } # Test order of evaluation compared to default_post, CONFIG, etc. sub test_ordering { my ($proj) = @_; my $dir = "$TESTDATA/06-ordering"; local $CWD = $dir; run_qmake( ); if ($proj) { $proj->set_makefile( 'Makefile' ); } else { $proj = QMake::Project->new( 'Makefile' ); } my @DEFINES = $proj->values( 'DEFINES' ); # If we correctly evaluate after default_post.prf and all CONFIG are # processed, then our setting of QT_NAMESPACE in our .pro file has # been transformed into a DEFINE. Otherwise, it hasn't. is( grep( { $_ =~ qr/QT_NAMESPACE=QMake_Project/ } @DEFINES ), 1 ) || diag "DEFINES: @DEFINES"; return; } # Test what happens when an error occurs somewhere sub test_error { my ($proj) = @_; my $dir = "$TESTDATA/04-error"; local $CWD = $dir; run_qmake( ); # qmake worked the first time, but this environment # variable will force an error when we try to parse it. local $ENV{ FORCE_QMAKE_ERROR } = 'fake error'; if ($proj) { $proj->set_makefile( 'Makefile' ); } else { $proj = QMake::Project->new( 'Makefile' ); } my $uses_gui = $proj->test( 'contains(QT,gui)' ); throws_ok( sub { $uses_gui = "$uses_gui" }, $ERROR_RE ); # If we set_die_on_error 0, this should become a warning. my $qt = $proj->values( 'QT' ); $proj->set_die_on_error( 0 ); warnings_like { "$qt" } [ $ERROR_RE, qr{Use of uninitialized value} ]; my @qt; warning_like { @qt = $proj->values( 'QT' ) } $ERROR_RE; is( scalar(@qt), 0 ); # Test that turning errors back on really works $proj->set_die_on_error( 1 ); throws_ok( sub { diag( 'unix: '.$proj->test( 'unix' ) ) }, $ERROR_RE ); return; } # Test spaces in path and value sub test_spaces { my ($proj) = @_; my $dir = "$TESTDATA/05 space in path"; local $CWD = $dir; run_qmake( ); if ($proj) { $proj->set_makefile( "$dir/Makefile" ); } else { $proj = QMake::Project->new( "$dir/Makefile" ); } my $initial_count = $proj->{ _qmake_count }; is( $proj->values( 'TEMPLATE' ), 'app' ); is( $proj->values( 'TARGET' ), '05 space in path' ); is( canonpath( $proj->values( 'PWD' ) ), canonpath( $dir ) ); is( canonpath( $proj->values( 'OUT_PWD' ) ), canonpath( $dir ) ); is_deeply( [ $proj->values( 'SOURCES' ) ], [ 'main.cpp', 'space in source.cpp' ], ); is( $proj->{ _qmake_count } - $initial_count, 5 ); return; } # Test a few really basic error conditions, e.g. make itself can't work sub test_make_error { my ($proj) = @_; my $dir = "$TESTDATA/01-typical"; local $CWD = $dir; run_qmake( ); if ($proj) { $proj->set_makefile( undef ); } else { $proj = QMake::Project->new( ); } my $sub = sub{ my @qt = $proj->values( 'QT' ) }; my $original_make = $proj->make( ); # fail: no makefile set throws_ok( sub { $sub->() }, # weird syntax is necessary to satisfy prototype qr{$ERROR_RE no makefile set} ); # fail: invalid makefile directory $proj->set_makefile( '/some/directory/does/not/exist' ); throws_ok( sub { $sub->() }, qr{$ERROR_RE.*No such file or directory} ); # fail: invalid makefile $proj->set_makefile( "$dir/Makefile.does-not-exist" ); throws_ok( sub { $sub->() }, $ERROR_RE, # generic error because make output is platform-specific ); # fail: bogus make command $proj->set_makefile( "$dir/Makefile" ); $proj->set_make( 'not-a-real-make-command' ); throws_ok( sub { $sub->() }, qr{$ERROR_RE .*not-a-real-make-command} ); $proj->set_make( $original_make ); # Now do it all again with warnings instead $proj->set_die_on_error( 0 ); # warn: no makefile set $proj->set_makefile( undef ); warning_like( sub { $sub->() }, qr{$ERROR_RE no makefile set} ); # warn: invalid makefile directory $proj->set_makefile( '/some/directory/does/not/exist' ); warning_like( sub { $sub->() }, qr{$ERROR_RE.*No such file or directory} ); # warn: invalid makefile $proj->set_makefile( "$dir/Makefile.does-not-exist" ); warning_like( sub { $sub->() }, $ERROR_RE, # generic error because make output is platform-specific ); # warn: bogus make command $proj->set_makefile( "$dir/Makefile" ); $proj->set_make( 'not-a-real-make-command' ); warning_like( sub { $sub->() }, qr{$ERROR_RE .*not-a-real-make-command} ); $proj->set_make( $original_make ); return; } sub run_test { unless ($QMAKE) { plan skip_all => 'No qmake found in PATH'; } test_typical; test_default_target; test_list; test_delayed; test_spaces; test_ordering; test_error; test_make_error; # Now do them all again using a single object my $proj = QMake::Project->new( ); test_typical( $proj ); test_default_target( $proj ); test_list( $proj ); test_delayed( $proj ); test_spaces( $proj ); test_ordering( $proj ); test_error( $proj ); test_make_error( $proj ); return; } sub find_qmake { my @qmakes = qw(qmake-qt5 qmake-qt4 qmake); foreach my $qmake (@qmakes) { if (my $found = which( $qmake )) { return $found; } } return; } if (!caller) { run_test; done_testing; } 1;