diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..2ae3719e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,112 @@ +--- +name: CI + +on: + push: + branches: + - develop + - master + - 'release/**' + pull_request: + branches: + - develop + - master + - 'release/**' + +jobs: + run-tests: + strategy: + matrix: + compatibility: + - develop + # - latest + perl: + - '5.40' + - '5.36' + - '5.26' + runner: + - ubuntu-22.04 + + runs-on: ${{ matrix.runner }} + + steps: + - uses: actions/checkout@v2 + + - uses: shogo82148/actions-setup-perl@v1 + with: + perl-version: ${{ matrix.perl }} + + - name: Install binary dependencies + run: | + # * These were taken from the installation instruction. + # * Gettext was added so we can run cpanm . on the Engine sources. + # * The Perl modules were left out because I couldn't get all of them + # to work with custom Perl versions. + # * Cpanminus was left out because actions-setup-perl installs it. + sudo apt-get install -y \ + autoconf \ + automake \ + build-essential \ + gettext \ + libidn2-dev \ + libssl-dev \ + libtool \ + m4 \ + + - name: Install Zonemaster dependencies (latest) + if: ${{ matrix.compatibility == 'latest' }} + run: | + cpanm --sudo --notest \ + Module::Install \ + ExtUtils::PkgConfig \ + Zonemaster::Engine + + - name: Install Zonemaster dependencies (develop) + if: ${{ matrix.compatibility == 'develop' }} + run: | + cpanm --sudo --notest \ + Devel::CheckLib \ + Module::Install \ + ExtUtils::PkgConfig \ + Module::Install::XSUtil + git clone --branch=develop --depth=1 \ + https://github.com/zonemaster/zonemaster-ldns.git + perl Makefile.PL # Generate MYMETA.yml to appease cpanm . + ( cd zonemaster-ldns ; cpanm --sudo --notest . ) + rm -rf zonemaster-ldns + git clone --branch=develop --depth=1 \ + https://github.com/zonemaster/zonemaster-engine.git + perl Makefile.PL # Generate MYMETA.yml to appease cpanm . + ( cd zonemaster-engine ; cpanm --sudo --notest . ) + rm -rf zonemaster-engine + + # Installing Zonemaster::Engine requires root privileges, because of a + # bug in Mail::SPF preventing normal installation with cpanm as + # non-root user (see link below [1]). + # + # The alternative, if one still wishes to install Zonemaster::Engine + # as non-root user, is to install Mail::SPF first with a command like: + # + # % cpanm --notest \ + # --install-args="--install_path sbin=$HOME/.local/sbin" \ + # Mail::SPF + # + # For the sake of consistency, other Perl packages installed from CPAN + # are also installed as root. + # + # [1]: https://rt.cpan.org/Public/Bug/Display.html?id=34768 + - name: Install remaining dependencies + run: | + cpanm --sudo --verbose --notest --installdeps . + + - name: Install Zonemaster::CLI + run: | + cpanm --sudo --verbose --notest . + + - name: Show content of log files + if: ${{ failure() }} + run: cat /home/runner/.cpanm/work/*/build.log + + - name: Test + run: | + prove -lv t diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index fbdaaa01..00000000 --- a/.travis.yml +++ /dev/null @@ -1,73 +0,0 @@ -dist: jammy -group: previous - -language: perl - -perl: - - "5.38" - - "5.34" - - "5.26" - -addons: - apt: - packages: - # From Zonemaster Engine installation instruction for Ubuntu - - autoconf - - automake - - build-essential - - cpanminus - - libclone-perl - - libdevel-checklib-perl - - libextutils-pkgconfig-perl - - libfile-sharedir-perl - - libfile-slurp-perl - - libidn2-dev - - libintl-perl - - libjson-pp-perl - - liblist-compare-perl - - liblist-moreutils-perl - - liblocale-msgfmt-perl - - libmail-rfc822-address-perl - - libmail-spf-perl - - libmodule-find-perl - - libnet-ip-perl - - libpod-coverage-perl - - libreadonly-xs-perl - - libssl-dev - - libtest-differences-perl - - libtest-exception-perl - - libtest-fatal-perl - - libtest-pod-perl - - libtext-csv-perl - - libtool - - m4 - # From Zonemaster CLI installation instruction for Ubuntu - # libmodule-install-perl, see cpan-install below - - libtry-tiny-perl - -before_install: - # Help Perl find modules installed from OS packages -- export PERL5LIB=/usr/share/perl5 - - # Provide cpanm helper - # quoting preserves newlines in the script and then avoid error if the script contains comments -- eval "$(curl https://travis-perl.github.io/init)" --auto - - # Zonemaster LDNS needs a newer version of Module::Install -- cpan-install Module::Install Module::Install::XSUtil - - # IO::Socket::INET6 can't find Socket6 installed from OS package -- cpan-install Socket6 IO::Socket::INET6 - - # Install Zonemaster LDNS -- git clone --depth=1 --branch=$TRAVIS_BRANCH https://github.com/zonemaster/zonemaster-ldns.git -- ( cd zonemaster-ldns && cpanm --verbose --notest --configure-args="--no-ed25519" . ) && rm -rf zonemaster-ldns - - # Install Zonemaster Engine -- git clone --depth=1 --branch=$TRAVIS_BRANCH https://github.com/zonemaster/zonemaster-engine.git -- ( cd zonemaster-engine && cpanm --verbose --notest . ) && rm -rf zonemaster-engine - - # Fix Header files location issue -- if [[ ! -e /usr/include/sys/ ]]; then sudo mkdir /usr/include/sys/; fi -- if [[ ! -e /usr/include/bits/ && -e /usr/include/x86_64-linux-gnu/bits/ ]]; then sudo ln -s /usr/include/x86_64-linux-gnu/bits/ /usr/include/bits; fi -- if [[ ! -e /usr/include/sys/socket.h ]]; then sudo ln -s /usr/include/bits/socket.h /usr/include/sys/socket.h; fi diff --git a/Changes b/Changes index d8edfb97..72d07c86 100644 --- a/Changes +++ b/Changes @@ -1,6 +1,20 @@ Release history for Zonemaster component Zonemaster-CLI +v8.0.0 2025-06-26 (part of Zonemaster v2025.1 release) + + [Breaking changes] +- Makes the --test option more flexible #359 + + [Features] +- Expands the --nstimes option #421 +- Expands the --count option #424 + + [Fixes] +- Updates translations #444, #445 +- Slows down the spinner a bit #419 + + v7.2.0 2025-03-04 (part of Zonemaster v2024.2.1 release) [Release information] diff --git a/MANIFEST b/MANIFEST index 9abca490..29e0fce7 100644 --- a/MANIFEST +++ b/MANIFEST @@ -12,6 +12,7 @@ inc/Module/Install/Share.pm inc/Module/Install/Win32.pm inc/Module/Install/WriteAll.pm lib/Zonemaster/CLI.pm +lib/Zonemaster/CLI/TestCaseSet.pm LICENSE Makefile.PL MANIFEST This list of files @@ -29,6 +30,7 @@ share/locale/sl/LC_MESSAGES/Zonemaster-CLI.mo share/locale/sv/LC_MESSAGES/Zonemaster-CLI.mo t/00-load.t t/pod.t +t/test_case_set.t t/usage.fake-data.data t/usage.fake-root.data t/usage.hints diff --git a/Makefile.PL b/Makefile.PL index e51c32f2..75e3e39e 100644 --- a/Makefile.PL +++ b/Makefile.PL @@ -23,8 +23,8 @@ requires( 'JSON::XS' => 0, 'Locale::TextDomain' => 1.23, 'Try::Tiny' => 0, - 'Zonemaster::LDNS' => 4.001000, # v4.1.0 - 'Zonemaster::Engine' => 7.001000, # v7.1.0 + 'Zonemaster::LDNS' => 5.000000, # v5.0.0 + 'Zonemaster::Engine' => 8.000000, # v8.0.0 ); test_requires( diff --git a/lib/Zonemaster/CLI.pm b/lib/Zonemaster/CLI.pm index 905c4ccd..02eaa608 100644 --- a/lib/Zonemaster/CLI.pm +++ b/lib/Zonemaster/CLI.pm @@ -1,18 +1,17 @@ # Brief help module to define the exception we use for early exits. package Zonemaster::Engine::Exception::NormalExit; -use 5.014002; +use v5.26; use warnings; use parent 'Zonemaster::Engine::Exception'; # The actual interesting module. package Zonemaster::CLI; -use 5.014002; +use v5.26; -use strict; use warnings; -use version; our $VERSION = version->declare( "v7.2.0" ); +use version; our $VERSION = version->declare( "v8.0.0" ); use Locale::TextDomain 'Zonemaster-CLI'; @@ -26,15 +25,17 @@ use Pod::Usage; use POSIX qw[setlocale LC_MESSAGES LC_CTYPE]; use Readonly; use Scalar::Util qw[blessed]; +use Time::HiRes; use Try::Tiny; -use Zonemaster::LDNS; -use Zonemaster::Engine; +use Zonemaster::CLI::TestCaseSet; use Zonemaster::Engine::Exception; -use Zonemaster::Engine::Normalization qw[normalize_name]; use Zonemaster::Engine::Logger::Entry; +use Zonemaster::Engine::Normalization qw[normalize_name]; use Zonemaster::Engine::Translator; -use Zonemaster::Engine::Util qw[parse_hints]; +use Zonemaster::Engine::Util qw[parse_hints]; use Zonemaster::Engine::Validation qw[validate_ipv4 validate_ipv6]; +use Zonemaster::Engine; +use Zonemaster::LDNS; our %numeric = Zonemaster::Engine::Logger::Entry->levels; our $JSON = JSON::XS->new->allow_blessed->convert_blessed->canonical; @@ -141,10 +142,11 @@ sub run { 'test=s' => \@opt_test, 'time!' => \$opt_time, 'version!' => \$opt_version, - ) or do { + ) + or do { my_pod2usage( verbosity => 0, output => \*STDERR ); return 2; - }; + }; } if ( $opt_help ) { @@ -167,17 +169,20 @@ sub run { $ENV{LC_ALL} = $opt_locale; } - # Set LC_MESSAGES and LC_CTYPE separately (https://www.gnu.org/software/gettext/manual/html_node/Triggering.html#Triggering) + # Set LC_MESSAGES and LC_CTYPE separately + # (https://www.gnu.org/software/gettext/manual/html_node/Triggering.html#Triggering) if ( not defined setlocale( LC_MESSAGES, "" ) ) { - my $locale = ($ENV{LANGUAGE} || $ENV{LC_ALL} || $ENV{LC_MESSAGES}); - say STDERR __x( "Warning: setting locale category LC_MESSAGES to {locale} failed -- is it installed on this system?\n\n", - locale => $locale) + my $locale = ( $ENV{LANGUAGE} || $ENV{LC_ALL} || $ENV{LC_MESSAGES} ); + say STDERR __x( + "Warning: setting locale category LC_MESSAGES to {locale} failed -- is it installed on this system?", + locale => $locale ) . "\n\n"; } - + if ( not defined setlocale( LC_CTYPE, "" ) ) { - my $locale = ($ENV{LC_ALL} || $ENV{LC_CTYPE}); - say STDERR __x( "Warning: setting locale category LC_CTYPE to {locale} failed -- is it installed on this system?\n\n", - locale => $locale) + my $locale = ( $ENV{LC_ALL} || $ENV{LC_CTYPE} ); + say STDERR __x( + "Warning: setting locale category LC_CTYPE to {locale} failed -- is it installed on this system?", + locale => $locale ) . "\n\n"; } if ( $opt_version ) { @@ -202,7 +207,8 @@ sub run { if ( defined $opt_json_translate ) { unless ( $opt_json or $opt_json_stream ) { - printf STDERR __( "Warning: --json-translate has no effect without either --json or --json-stream." ) . "\n"; + printf STDERR __( "Warning: --json-translate has no effect without either --json or --json-stream." ) + . "\n"; } if ( $opt_json_translate ) { printf STDERR __( "Warning: deprecated --json-translate, use --no-raw instead." ) . "\n"; @@ -254,97 +260,27 @@ sub run { }; } - my @testing_suite; - if ( @opt_test ) { - my %existing_tests = Zonemaster::Engine->all_methods; - my @existing_test_modules = keys %existing_tests; - my @existing_test_cases = map { @{ $existing_tests{$_} } } @existing_test_modules; + { + my %all_methods = Zonemaster::Engine->all_methods; + my $cases = Zonemaster::CLI::TestCaseSet->new( # + Zonemaster::Engine::Profile->effective->get( q{test_cases} ), + \%all_methods, + ); - foreach my $t ( @opt_test ) { - # There should be at most one slash character - if ( $t =~ tr/\/// > 1 ) { - say STDERR __x( "Error: Invalid input '{cli_arg}' in --test. There must be at most one slash ('/') character.", - cli_arg => $t); - return $EXIT_USAGE_ERROR; - } + for my $test ( @opt_test ) { + my @modifiers = Zonemaster::CLI::TestCaseSet->parse_modifier_expr( $test ); + while ( @modifiers ) { + my $op = shift @modifiers; + my $term = shift @modifiers; - # The case does not matter - $t = lc( $t ); - - my ( $module, $method ); - # Fully qualified module and test case (e.g. Example/example12), or just a test case (e.g. example12). Note the different capturing order. - if ( ( ($module, $method) = $t =~ m#^ ( [a-z]+ ) / ( [a-z]+[0-9]{2} ) $#ix ) - or - ( ($method, $module) = $t =~ m#^ ( ( [a-z]+ ) [0-9]{2} ) $#ix ) ) - { - # Check that test module exists - if ( grep( /^$module$/, map { lc($_) } @existing_test_modules ) ) { - # Check that test case exists - if ( grep( /^$method$/, @existing_test_cases ) ) { - push @testing_suite, "$module/$method"; - } - else { - say STDERR __x( "Error: Unrecognized test case '{testcase}' in --test. Use --list-tests for a list of valid choices.", - testcase => $method ); - return $EXIT_USAGE_ERROR; - } - } - else { - say STDERR __x( "Error: Unrecognized test module '{module}' in --test. Use --list-tests for a list of valid choices.", - module => $module ); - return $EXIT_USAGE_ERROR; - } - } - # Just a module name (e.g. Example) or something invalid. - else { - $t =~ s{/$}{}; - # Check that test module exists - if ( grep( /^$t$/, map { lc($_) } @existing_test_modules ) ) { - push @testing_suite, $t; - } - else { - say STDERR __x( "Error: Invalid input '{cli_arg}' in --test.", - cli_arg => $t); + if ( !$cases->apply_modifier( $op, $term ) ) { + say STDERR __x( "Error: unrecognized term '{term}' in --test.", term => $term ) . "\n"; return $EXIT_USAGE_ERROR; } } } - # Start with all profile-enabled test cases - my @actual_test_cases = @{ Zonemaster::Engine::Profile->effective->get( 'test_cases' ) }; - - # Derive test module from each profile-enabled test case - my %actual_test_modules; - foreach my $t ( @actual_test_cases ) { - my ( $module ) = $t =~ m#^ ( [a-z]+ ) [0-9]{2} $#ix; - $actual_test_modules{$module} = 1; - } - - # Check if more test cases need to be included in the profile - foreach my $t ( @testing_suite ) { - # Either a module/method, or just a module - my ( $module, $method ) = split('/', $t); - if ( $method ) { - # Test case in not already in the profile, we add it explicitly and notify the user - if ( not grep( /^$method$/, @actual_test_cases ) ) { - say $fh_diag __x( "Notice: Engine does not have test case '{testcase}' enabled in the profile. Forcing...", - testcase => $method ); - push @actual_test_cases, $method; - } - } - else { - # No test case from this module is already in the profile, we can add them all - if ( not grep( /^$module$/, keys %actual_test_modules ) ) { - # Get the test module with the right case - ( $module ) = grep { lc( $module ) eq lc( $_ ) } @existing_test_modules; - # No need to bother to check for duplicates here - push @actual_test_cases, @{ $existing_tests{$module} }; - } - } - } - - # Configure Engine to include all of the required test cases in the profile - Zonemaster::Engine::Profile->effective->set( 'test_cases', [ uniq sort @actual_test_cases ] ); + Zonemaster::Engine::Profile->effective->set( q{test_cases}, [ $cases->to_list ] ), } # These two must come after any profile from command line has been loaded @@ -390,7 +326,7 @@ sub run { module => 12, testcase => 14 ); - my %header_names = (); + my %header_names = (); my %remaining_space = (); # Callback defined here so it closes over the setup above. @@ -400,9 +336,11 @@ sub run { print_spinner() if $show_progress; - $counter{ uc $entry->level } += 1; + my $entry_level = $entry->level; + + $counter{ uc $entry_level } += 1; - if ( $numeric{ uc $entry->level } >= $numeric{$opt_level} ) { + if ( $numeric{ uc $entry_level } >= $numeric{$opt_level} ) { $printed_something = 1; if ( $opt_json and $opt_json_stream ) { @@ -412,8 +350,8 @@ sub run { $r{module} = $entry->module if $opt_show_module; $r{testcase} = $entry->testcase if $opt_show_testcase; $r{tag} = $entry->tag; - $r{level} = $entry->level if $opt_show_level; - $r{args} = $entry->args if $entry->args; + $r{level} = $entry_level if $opt_show_level; + $r{args} = $entry->args if $entry->args; $r{message} = $translator->translate_tag( $entry ) unless $opt_raw; say $JSON->encode( \%r ); @@ -424,29 +362,29 @@ sub run { else { my $prefix = q{}; if ( $opt_time ) { - $prefix .= sprintf "%*.2f ", ${field_width{seconds}}, $entry->timestamp; + $prefix .= sprintf "%*.2f ", ${ field_width { seconds } }, $entry->timestamp; } if ( $opt_show_level ) { $prefix .= $opt_raw ? $entry->level : translate_severity( $entry->level ); my $space_l10n = - ${ field_width { level } } - length( decode_utf8( translate_severity( $entry->level ) ) ) + 1; + ${ field_width { level } } - length( decode_utf8( translate_severity( $entry_level ) ) ) + 1; $prefix .= ' ' x $space_l10n; } if ( $opt_show_module ) { - $prefix .= sprintf "%-*s ", ${field_width{module}}, $entry->module; + $prefix .= sprintf "%-*s ", ${ field_width { module } }, $entry->module; } if ( $opt_show_testcase ) { - $prefix .= sprintf "%-*s ", ${field_width{testcase}}, $entry->testcase; + $prefix .= sprintf "%-*s ", ${ field_width { testcase } }, $entry->testcase; } if ( $opt_raw ) { $prefix .= $entry->tag; my $message = $entry->argstr; - my @lines = split /\n/, $message; + my @lines = split /\n/, $message; printf "%s%s %s\n", $prefix, ' ', @lines ? shift @lines : ''; for my $line ( @lines ) { @@ -454,8 +392,11 @@ sub run { } } else { - if ( $entry->level eq q{DEBUG3} and scalar( keys %{$entry->args} ) == 1 and defined $entry->args->{packet} ) { - my $packet = $entry->args->{packet}; + if ( $entry_level eq q{DEBUG3} + and scalar( keys %{ $entry->args } ) == 1 + and defined $entry->args->{packet} ) + { + my $packet = $entry->args->{packet}; my $padding = q{ } x length $prefix; $entry->args->{packet} = q{}; printf "%s%s\n", $prefix, $translator->translate_tag( $entry ); @@ -467,10 +408,14 @@ sub run { printf "%s%s\n", $prefix, $translator->translate_tag( $entry ); } } - } - } + } ## end else [ if ( $opt_json and $opt_json_stream)] + } ## end if ( $numeric{ uc $entry_level...}) if ( $opt_stop_level and $numeric{ uc $entry->level } >= $numeric{$opt_stop_level} ) { - die( Zonemaster::Engine::Exception::NormalExit->new( { message => "Saw message at level " . $entry->level } ) ); + die( + Zonemaster::Engine::Exception::NormalExit->new( + { message => "Saw message at level " . $entry->level } + ) + ); } }; @@ -484,7 +429,6 @@ sub run { } ); - if ( @argv > 1 ) { say STDERR __( "Only one domain can be given for testing. Did you forget to prepend an option with '--