#!/usr/bin/perl BEGIN { die "The PERCONA_TOOLKIT_BRANCH environment variable is not set.\n" unless $ENV{PERCONA_TOOLKIT_BRANCH} && -d $ENV{PERCONA_TOOLKIT_BRANCH}; unshift @INC, "$ENV{PERCONA_TOOLKIT_BRANCH}/lib"; }; use strict; use warnings FATAL => 'all'; use English qw(-no_match_vars); use Test::More tests => 107; use PerconaTest; use OptionParser; use File::Spec; use File::Temp (); BEGIN { use_ok "Diskstats"; use_ok "DiskstatsGroupByAll"; use_ok "DiskstatsGroupByDisk"; use_ok "DiskstatsGroupBySample"; } my $o = new OptionParser(description => 'Diskstats'); $o->get_specs("$trunk/bin/pt-diskstats"); $o->get_opts(); { my $obj = new Diskstats(OptionParser => $o); can_ok( $obj, qw( columns_regex devices_regex filename block_size ordered_devs clear_state clear_ordered_devs stats_for prev_stats_for first_stats_for has_stats design_print_formats parse_diskstats_line parse_from print_deltas ) ); # ############################################################################ # Testing the constructor. # ############################################################################ for my $attr ( [ filename => (File::Temp::tempfile($0.'diskstats.XXXXXX', OPEN=>0, UNLINK=>1))[1] ], [ columns_regex => qr/!!!/ ], [ devices_regex => qr/!!!/ ], [ block_size => 215 ], [ show_inactive => 1 ], [ sample_time => 1 ], [ interactive => 1 ], ) { my $attribute = $attr->[0]; my $value = $attr->[1]; my $test_obj = Diskstats->new( @$attr, OptionParser => $o ); is( $test_obj->$attribute(), $value, "Passing an explicit [$attribute] to the constructor works", ); } # ############################################################################ # parse_diskstats_line # ############################################################################ for my $test ( [ "104 0 cciss/c0d0 2139885 162788 37361471 8034486 17999682 83425310 811400340 12711047 0 6869437 20744582", [ 104, 0, "cciss/c0d0", # major, minor, device 2139885, # reads 162788, # reads_merged 37361471, # read_sectors 8034486, # ms_spent_reading 17999682, # writes 83425310, # writes_merged 811400340, # written_sectors 12711047, # ms_spent_writing 0, # ios_in_progress 6869437, # ms_spent_doing_io 20744582, # ms_weighted 18680735.5, # read_kbs 405700170, # written_kbs 103727665, # ios_requested 434566047232,# ios_in_bytes ], "parse_diskstats_line works" ], [ " 8 33 sdc1 1572537676 2369344 3687151364 1575056414 2541895139 1708184481 3991989096 121136333 1 982122453 1798311795", [ '8', '33', 'sdc1', 1572537676, '2369344', 3687151364, '1575056414', 2541895139, '1708184481', 3991989096, '121136333', '1', '982122453', '1798311795', '1843575682', '1995994548', 5824986640, '3931719915520' ], "parse_diskstats_line works" ], [ " 8 33 sdc1 1572537676 2369344 3687151364 1575056414 2541895139 1708184481 3991989096 121136333 1 982122453 1798311795\n", [ '8', '33', 'sdc1', 1572537676, '2369344', 3687151364, '1575056414', 2541895139, '1708184481', 3991989096, '121136333', '1', '982122453', '1798311795', '1843575682', '1995994548', 5824986640, '3931719915520' ], "parse_diskstats_line ignores a trailing newline" ], [ " 8 33 sdc1 1572537676 2369344 3687151364 1575056414 2541895139 1708184481 3991989096 121136333 1 982122453 \n", undef, "parse_diskstats_line fails on a line without enough fields" ], [ " 8 33 sdc1 1572537676 2369344 3687151364 1575056414 2541895139 1708184481 3991989096 121136333 1 982122453 12224123 12312312", undef, "parse_diskstats_line fails on a line with too many fields" ], [ "", undef, "parse_diskstats_line returns undef on an empty string", ], [ "Malformed line", undef, "parse_diskstats_line returns undef on a malformed line", ], ) { my ($line, $expected_results, $desc) = @$test; my ($dev, $res) = $obj->parse_diskstats_line($line, $obj->block_size); is_deeply( $res, $expected_results, $desc ); } # For speed, ->parse_diskstats_line doesn't check for undef. # In any case, this should never happen, since it's internally # used within a readline() loop. local $EVAL_ERROR; eval { $obj->parse_diskstats_line(undef, $obj->block_size); }; like( $EVAL_ERROR, qr/Use of uninitialized value/, "parse_diskstats_line should fail on undef", ); # ############################################################################ # design_print_formats # ############################################################################ my @columns_in_order = @Diskstats::columns_in_order; $obj->set_columns_regex(qr/./); my ($header, $rows, $cols) = $obj->design_print_formats(); is_deeply( $cols, [ map { $_->[0] } @columns_in_order ], "design_print_formats: returns the expected columns" ); # qr/ \A (?!.*io_s$|\s*[qs]time$) /x $obj->set_columns_regex(qr/cnc|rt|busy|prg|[mk]b|[dr]_s|mrg/); ($header, $rows, $cols) = $obj->design_print_formats(); is( $header, join(" ", q{%+*s %-6s}, grep { $_ =~ $obj->columns_regex() } map { $_->[0] } @columns_in_order), "design_print_formats: sanity check for defaults" ); $obj->set_columns_regex(qr/./); ($header, $rows, $cols) = $obj->design_print_formats(max_device_length => 10); my $all_columns_format = join(" ", q{%+*s %-10s}, map { $_->[0] } @columns_in_order); is( $header, $all_columns_format, "design_print_formats: max_device_length works" ); $obj->set_columns_regex(qr/(?!)/); # Will never match ($header, $rows, $cols) = $obj->design_print_formats(max_device_length => 10); is( $header, q{%+*s %-10s }, "design_print_formats respects columns_regex" ); $obj->set_columns_regex(qr/./); ($header, $rows, $cols) = $obj->design_print_formats( max_device_length => 10, columns => [] ); is( $header, q{%+*s %-10s }, "...unless we pass an explicit column array" ); $obj->set_columns_regex(qr/./); ($header, $rows, $cols) = $obj->design_print_formats( max_device_length => 10, columns => [qw( busy )] ); is( $header, q{%+*s %-10s busy}, "Header" ); ($header, $rows, $cols) = $obj->design_print_formats( max_device_length => 10, columns => [ map { $_->[0] } @columns_in_order ], ); is( $header, $all_columns_format, "All columns format" ); throws_ok( sub { $obj->design_print_formats( columns => {} ) }, qr/The columns argument to design_print_formats should be an arrayref/, "design_print_formats dies when passed an invalid columns argument"); # ############################################################################ # timestamp methods # ############################################################################ for my $method ( qw( curr_ts prev_ts first_ts ) ) { my $setter = "set_$method"; ok(!$obj->$method(), "Diskstats->$method is initially false"); $obj->$setter(10); is($obj->$method(), 10, "Diskstats->$setter(10) sets it to 10"); $obj->$setter(20); $obj->clear_ts(); ok(!$obj->$method(), "Diskstats->clear_ts does as advertized"); } # ############################################################################ # Adding, removing and listing devices. # ############################################################################ is_deeply( [ $obj->ordered_devs() ], [], "ordered_devs starts empty" ); $obj->add_ordered_dev("sda"); is_deeply( [ $obj->ordered_devs() ], [ qw( sda ) ], "We can add devices just fine," ); $obj->add_ordered_dev("sda"); is_deeply( [ $obj->ordered_devs() ], [ qw( sda ) ], "...And duplicates get detected and discarded" ); $obj->clear_ordered_devs(); is_deeply( [ $obj->ordered_devs() ], [], "clear_ordered_devs does as advertized," ); $obj->add_ordered_dev("sda"); is_deeply( [ $obj->ordered_devs() ], [ qw( sda ) ], "...And clears the internal duplicate-checking list" ); # ############################################################################ # show_inactive -- Whenever it prints inactive devices. # ############################################################################ ## ## show_inactive now functions inside parse_from ## #$obj->set_show_inactive(0); #my $print_output = output( # sub { # $obj->print_rows( # "SHOULDN'T PRINT THIS", # [ qw( a b c ) ], # { a => 0, b => 0, c => 0, d => 10 } # ); # } #); #$obj->set_show_inactive(1); # #is( # $print_output, # "", # "->show_inactive works" #); # ############################################################################ # Sane defaults and fatal errors # ############################################################################ for my $method ( qw( delta_against delta_against_ts group_by ) ) { throws_ok( sub { Diskstats->$method() }, qr/\QYou must override $method() in a subclass\E/, "->$method has to be overriden" ); } is( $obj->compute_line_ts( first_ts => 0 ), sprintf( "%5.1f", 0 ), "compute_line_ts has a sane default", ); $obj->set_force_header(0); is( output( sub { $obj->print_header("asdasdas") } ), "", "force_header works" ); my $output = output( sub { $obj->parse_from( data => "ASMFHNASJNFASKLFLKHNSKD" ); }, stderr => 1, ); is( $output, "", "Doesn't die parsing unknown line" ); # ############################################################################ # _calc* methods # ############################################################################ $obj->clear_state(); my $prev = { TS => 1281367519, data => ($obj->parse_diskstats_line( "104 0 cciss/c0d0 2139885 162788 37361471 8034486 17999682 83425310 811400340 12711047 0 6869437 20744582", $obj->block_size))[1] }; my $curr = { TS => 1281367521, data => ($obj->parse_diskstats_line( "104 0 cciss/c0d0 2139886 162790 37361478 8034489 17999738 83425580 811402798 12711097 3 6869449 20744632", $obj->block_size))[1] }; $obj->first_ts( $prev->{TS} ); $obj->prev_ts( $prev->{TS} ); $obj->curr_ts( $curr->{TS} ); my $deltas = $obj->_calc_delta_for($curr->{data}, $prev->{data}); is_deeply( $deltas, { ms_spent_doing_io => 12, ms_spent_reading => 3, ms_spent_writing => 50, ms_weighted => 50, read_kbs => 3.5, read_sectors => 7, reads => 1, reads_merged => 2, writes => 56, writes_merged => 270, written_kbs => 1229, written_sectors => 2458, ios_in_bytes => 1262080, ios_requested => 329, ios_in_progress => 3, }, "_calc_delta_for works" ); local $EVAL_ERROR; eval { $obj->_calc_delta_for($curr->{data}, []) }; ok(!$EVAL_ERROR, "_calc_delta_for guards against undefined values"); my %read_stats = $obj->_calc_read_stats( delta_for => $deltas, elapsed => $curr->{TS} - $prev->{TS}, devs_in_group => 1, ); is_deeply( \%read_stats, { avg_read_sz => '3.5', mbytes_read_sec => '0.001708984375', read_conc => '0.0015', read_merge_pct => '66.6666666666667', read_requests => 3, read_rtime => 1, reads_sec => '0.5' }, "_calc_read_stats works" ); my %write_stats = $obj->_calc_write_stats( delta_for => $deltas, elapsed => $curr->{TS} - $prev->{TS}, devs_in_group => 1, ); is_deeply( \%write_stats, { avg_write_sz => '21.9464285714286', mbytes_written_sec => '0.60009765625', write_conc => '0.025', write_merge_pct => '82.8220858895706', write_requests => 326, write_rtime => '0.153374233128834', writes_sec => '28', }, "_calc_write_stats works" ); my %misc_stats = $obj->_calc_misc_stats( delta_for => $deltas, elapsed => $curr->{TS} - $prev->{TS}, devs_in_group => 1, stats => { %write_stats, %read_stats }, ); is_deeply( \%misc_stats, { busy => '0.6', line_ts => ' 0.0', qtime => '0.114128245504816', s_spent_doing_io => '28.5', stime => '0.0364741641337386', }, "_calc_misc_stats works" ); $obj->clear_state(); } # ############################################################################ # The three subclasses # ############################################################################ for my $test ( { class => "DiskstatsGroupByAll", results_file_prefix => "all", }, { class => "DiskstatsGroupByDisk", results_file_prefix => "disk", }, { class => "DiskstatsGroupBySample", results_file_prefix => "sample", }) { my $obj = $test->{class}->new(OptionParser => $o, show_inactive => 1); my $prefix = $test->{results_file_prefix}; $obj->set_columns_regex(qr/./); $obj->set_show_inactive(1); $obj->set_show_timestamps(0); $obj->set_automatic_headers(0); $obj->set_show_line_between_samples(0); for my $filename ( map "diskstats-00$_.txt", 1..5 ) { my $file = File::Spec->catfile( "t", "pt-diskstats", "samples", $filename ); my $file_with_trunk = File::Spec->catfile( $trunk, $file ); my $expected = load_file( File::Spec->catfile( "t", "pt-diskstats", "expected", "${prefix}_$filename" ) ); my $got = output( sub { $obj->group_by( filename => $file_with_trunk, ); }); is($got, $expected, "group_by $prefix: $filename via filename"); $got = output( sub { open my $fh, "<", $file_with_trunk or die $!; $obj->group_by( filehandle => $fh, ); }); is($got, $expected, "group_by $prefix: $filename via filehandle"); $got = output( sub { $obj->group_by( data => "TS 1298130002.073935000\n" . load_file( $file ), ); }); is($got, $expected, "group_by $prefix: $filename with an extra TS at the top"); } my $data = <<'EOF'; TS 1297205887.156653000 1 0 ram0 0 0 0 0 0 0 0 0 0 0 0 TS 1297205888.161613000 EOF my $got = output( sub { $obj->group_by(data => $data) }, stderr => 1 ); is( $got, '', "group_by $prefix: 1 line of data between two TS lines results in no output" ); $obj->set_curr_ts(0); $obj->set_prev_ts(0); $obj->set_first_ts(0); throws_ok( sub { $obj->_calc_deltas() }, qr/Time between samples should be > 0, is /, "$test->{class}, ->_calc_deltas fails if the time elapsed is 0" ); $obj->set_curr_ts(0); $obj->set_prev_ts(4); $obj->set_first_ts(4); throws_ok( sub { $obj->_calc_deltas() }, qr/Time between samples should be > 0, is /, "$test->{class}, ->_calc_deltas fails if the time elapsed is negative" ); } # ########################################################################### # Done. # ########################################################################### exit;