TER1 (Statement): 98.78%
TER2 (Branch): 76.92%
TER3 (LCSAJ): 100.0% (2/2)
Approximate LCSAJ segments: 27
● Covered — this LCSAJ path was executed during testing.
● Not covered — this LCSAJ path was never executed. These are the paths to focus on.
Multiple dots on a line indicate that multiple control-flow paths begin at that line. Hovering over any dot shows:
start → end → jump
Uncovered paths show [NOT COVERED] in the tooltip.
1: package Music::NWC2MusicXML::Diagnostics; 2: 3: use strict; 4: use warnings; 5: use autodie qw(:all); 6: 7: our $VERSION = '0.002.4'; 8: 9: use Carp qw(croak carp); 10: use Readonly; 11: use Params::Validate::Strict qw(validate_strict); 12: use Params::Get; 13: 14: # --------------------------------------------------------------------------- 15: # Log-level constants -- higher value = more output 16: # --------------------------------------------------------------------------- 17: Readonly::Scalar my $LOG_QUIET => 0; 18: Readonly::Scalar my $LOG_NORMAL => 1; 19: Readonly::Scalar my $LOG_VERBOSE => 2; 20: Readonly::Scalar my $LOG_DEBUG => 3; 21: 22: Readonly::Hash my %LOG_LEVEL_MAP => ( 23: quiet => $LOG_QUIET, 24: normal => $LOG_NORMAL, 25: verbose => $LOG_VERBOSE, 26: debug => $LOG_DEBUG, 27: ); 28: 29: # --------------------------------------------------------------------------- 30: # Exit-code constants (mirrored from main spec section 2) 31: # --------------------------------------------------------------------------- 32: Readonly::Scalar our $EXIT_OK => 0; 33: Readonly::Scalar our $EXIT_WARNINGS => 1; 34: Readonly::Scalar our $EXIT_BAD_INPUT => 2; 35: Readonly::Scalar our $EXIT_OUTPUT => 3; 36: Readonly::Scalar our $EXIT_INTERNAL => 4; 37: 38: # --------------------------------------------------------------------------- 39: # i18n message dictionary -- all user-visible strings live here 40: # --------------------------------------------------------------------------- 41: Readonly::Hash my %MESSAGES => ( 42: error_internal => 'Internal error: %s', 43: error_open_warn_file => 'Cannot open warnings file %s for writing: %s', 44: warn_unsupported_obj => '[%s] Staff %s at %s: unsupported NWC object %s (%s)', 45: warn_approx_feature => '[%s] Staff %s at %s: %s approximated as %s', 46: warn_no_equivalent => '[%s] Staff %s at %s: %s has no MusicXML equivalent -- preserved in diagnostics', 47: info_file_start => 'Converting: %s', 48: info_file_done => 'Done: %s -> %s', 49: info_file_failed => 'FAILED: %s (%s)', 50: info_summary => 'Files processed: %d Successful: %d Warnings: %d Failed: %d', 51: debug_decode_step => '[DEBUG] NWC decode: %s', 52: debug_parse_step => '[DEBUG] Parser: %s', 53: debug_gen_step => '[DEBUG] MusicXML gen: %s', 54: ); 55: 56: # --------------------------------------------------------------------------- 57: # new 58: # --------------------------------------------------------------------------- 59: 60: =head1 NAME 61: 62: Music::NWC2MusicXML::Diagnostics - Warning collection, logging, and reporting for 63: the Music::NWC2MusicXML conversion pipeline. 64: 65: =head1 VERSION 66: 67: 0.002.4 68: 69: =head1 SYNOPSIS 70: 71: use Music::NWC2MusicXML::Diagnostics; 72: 73: my $diag = Music::NWC2MusicXML::Diagnostics->new( 74: level => 'verbose', 75: warnings_fh => \*STDERR, 76: ); 77: 78: $diag->warn_unsupported( 79: file => 'Pilgrim.nwc', 80: staff => 'Staff 1', 81: pos => '4:2', 82: object => 'UserTool', 83: reason => 'No MusicXML equivalent', 84: ); 85: 86: $diag->summary; 87: 88: =head1 DESCRIPTION 89: 90: Centralises all diagnostic output for the Music::NWC2MusicXML pipeline. No 91: module should print warnings or debug traces directly; instead each module 92: receives a C<Diagnostics> instance and routes output through it. 93: 94: Supports four severity levels: quiet, normal (default), verbose, debug. 95: Warnings can be written to an optional file handle (C<--warnings FILE>). 96: A summary count (processed / successful / warnings / failed) is maintained 97: and can be printed at batch completion. 98: 99: =cut 100: 101: =head2 new 102: 103: Construct a Diagnostics instance. 104: 105: =head3 Arguments 106: 107: Named parameters: 108: 109: =over 4 110: 111: =item C<level> -- log verbosity (optional, default C<'normal'>). 112: 113: =item C<warnings_fh> -- writable filehandle for per-warning output (optional). 114: 115: =back 116: 117: =head3 Returns 118: 119: Blessed C<Music::NWC2MusicXML::Diagnostics> object. 120: 121: =head3 API SPECIFICATION 122: 123: =head4 Input 124: 125: level : SCALAR (optional, default 'normal') 126: -- Valid domain (4 values only, case-sensitive): 127: -- 'quiet', 'normal', 'verbose', 'debug' 128: -- Invalid: undef, '' (empty), wrong-case ('QUIET', 'Normal'), 129: -- numeric (0, 1, 2, 3), or any other string -> croak 130: -- error_internal 'Unknown log level: ...' 131: warnings_fh : filehandle (optional) 132: -- Valid: any writable filehandle, or undef/absent (no file output) 133: -- The caller retains ownership; this module never closes it 134: 135: =head4 Output 136: 137: Music::NWC2MusicXML::Diagnostics object 138: 139: =cut 140: 141: sub new { 142: my ($class, %input) = @_; 143: my $warnings_fh = delete $input{warnings_fh}; # glob refs can't be typed 144: my $args = validate_strict( 145: schema => { 146: level => { type => 'scalar', optional => 1, default => 'normal' }, 147: }, 148: input => \%input, 149: ); 150: croak $@ unless defined $args; 151: 152: my $level_key = $args->{level} // ''; 153: croak _fmt_msg('error_internal', 'Unknown log level: ' . ($level_key || '(undef)')) 154: unless exists $LOG_LEVEL_MAP{$level_key}; 155: 156: my $self = bless { 157: _level => $LOG_LEVEL_MAP{$level_key}, 158: _warnings_fh => $warnings_fh, 159: _warnings => [], 160: _counts => { processed => 0, successful => 0, warnings => 0, failed => 0 }, 161: }, $class; 162: 163: return $self;Mutants (Total: 2, Killed: 2, Survived: 0)
164: } 165: 166: # --------------------------------------------------------------------------- 167: # Public: logging methods 168: # --------------------------------------------------------------------------- 169: 170: =head2 info 171: 172: Log an informational message (suppressed at C<quiet> level). 173: 174: =head3 Purpose 175: 176: Emit progress / status information to STDERR. 177: 178: =head3 Arguments 179: 180: =over 4 181: 182: =item C<message> -- the text to emit. 183: 184: =back 185: 186: =head3 Returns 187: 188: C<$self> (for chaining). 189: 190: =head3 API SPECIFICATION 191: 192: =head4 Input 193: 194: message : SCALAR (required) 195: 196: =head4 Output 197: 198: $self (Music::NWC2MusicXML::Diagnostics) 199: 200: =head3 FORMAL SPECIFICATION 201: 202: [DiagInfo] 203: DiagInfo == message : String 204: 205: (placeholder -- populate with Z calculus as implementation matures) 206: 207: =cut 208: 209: sub info { 210: my ($self, $message) = @_; 211: return $self if $self->{_level} < $LOG_NORMAL;
212: print STDERR $message, "\n"; 213: return $self;Mutants (Total: 5, Killed: 2, Survived: 3)
- NUM_BOUNDARY_211_34_>: Numeric boundary flip < to >
HIGH: Likely missing edge-case test (boundary value)🧪 Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );- NUM_BOUNDARY_211_34_<=: Numeric boundary flip < to <=
HIGH: Likely missing edge-case test (boundary value)🧪 Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );- NUM_BOUNDARY_211_34_>=: Numeric boundary flip < to >=
HIGH: Likely missing edge-case test (boundary value)🧪 Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );Mutants (Total: 2, Killed: 2, Survived: 0)
214: } 215: 216: =head2 verbose 217: 218: Log a verbose message (emitted only at C<verbose> or C<debug> level). 219: 220: =head3 Purpose 221: 222: Emit per-file detail that is too chatty for normal output but useful when 223: diagnosing conversion issues. 224: 225: =head3 Arguments 226: 227: =over 4 228: 229: =item C<message> -- the text to emit. 230: 231: =back 232: 233: =head3 Returns 234: 235: C<$self>. 236: 237: =head3 API SPECIFICATION 238: 239: =head4 Input 240: 241: message : SCALAR (required) 242: 243: =head4 Output 244: 245: $self (Music::NWC2MusicXML::Diagnostics) 246: 247: =head3 FORMAL SPECIFICATION 248: 249: (placeholder) 250: 251: =cut 252: 253: sub verbose { 254: my ($self, $message) = @_; 255: return $self if $self->{_level} < $LOG_VERBOSE;
256: print STDERR $message, "\n"; 257: return $self;Mutants (Total: 5, Killed: 2, Survived: 3)
- NUM_BOUNDARY_255_34_>: Numeric boundary flip < to >
HIGH: Likely missing edge-case test (boundary value)🧪 Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );- NUM_BOUNDARY_255_34_<=: Numeric boundary flip < to <=
HIGH: Likely missing edge-case test (boundary value)🧪 Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );- NUM_BOUNDARY_255_34_>=: Numeric boundary flip < to >=
HIGH: Likely missing edge-case test (boundary value)🧪 Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );Mutants (Total: 2, Killed: 2, Survived: 0)
258: } 259: 260: =head2 debug 261: 262: Log a debug trace (emitted only at C<debug> level). 263: 264: =head3 Purpose 265: 266: Emit low-level pipeline tracing for developer use. 267: 268: =head3 Arguments 269: 270: =over 4 271: 272: =item C<message> -- the text to emit. 273: 274: =back 275: 276: =head3 Returns 277: 278: C<$self>. 279: 280: =head3 API SPECIFICATION 281: 282: =head4 Input 283: 284: message : SCALAR (required) 285: 286: =head4 Output 287: 288: $self (Music::NWC2MusicXML::Diagnostics) 289: 290: =head3 FORMAL SPECIFICATION 291: 292: (placeholder) 293: 294: =cut 295: 296: sub debug { 297: my ($self, $message) = @_; 298: return $self if $self->{_level} < $LOG_DEBUG;
299: print STDERR '[DEBUG] ', $message, "\n"; 300: return $self;Mutants (Total: 5, Killed: 2, Survived: 3)
- NUM_BOUNDARY_298_34_>: Numeric boundary flip < to >
HIGH: Likely missing edge-case test (boundary value)🧪 Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );- NUM_BOUNDARY_298_34_<=: Numeric boundary flip < to <=
HIGH: Likely missing edge-case test (boundary value)🧪 Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );- NUM_BOUNDARY_298_34_>=: Numeric boundary flip < to >=
HIGH: Likely missing edge-case test (boundary value)🧪 Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );Mutants (Total: 2, Killed: 2, Survived: 0)
301: } 302: 303: =head2 warn_unsupported 304: 305: Record a warning for an unsupported NWC object. 306: 307: =head3 Purpose 308: 309: Called when the parser or generator encounters an NWC object it cannot 310: represent in MusicXML. The warning is added to the internal list and, if 311: a C<warnings_fh> was supplied, written immediately to that handle. 312: 313: =head3 Arguments 314: 315: Named parameters (hashref or flat list): 316: 317: =over 4 318: 319: =item C<file> -- input filename (string, required). 320: 321: =item C<staff> -- staff name or number (string, required). 322: 323: =item C<pos> -- measure:beat position string (string, optional). 324: 325: =item C<object> -- NWC object type name (string, required). 326: 327: =item C<reason> -- human-readable reason (string, optional). 328: 329: =back 330: 331: =head3 Returns 332: 333: C<$self>. 334: 335: =head3 Side Effects 336: 337: Increments the internal warning counter. Writes to C<warnings_fh> if set. 338: 339: =head3 Usage Example 340: 341: $diag->warn_unsupported( 342: file => 'Pilgrim.nwc', 343: staff => 'Violin I', 344: pos => '12:1', 345: object => 'UserTool', 346: reason => 'No MusicXML equivalent', 347: ); 348: 349: =head3 API SPECIFICATION 350: 351: =head4 Input 352: 353: file : SCALAR (required) 354: staff : SCALAR (required) 355: pos : SCALAR (optional, default => '?') 356: object : SCALAR (required) 357: reason : SCALAR (optional, default => 'unknown') 358: 359: =head4 Output 360: 361: $self (Music::NWC2MusicXML::Diagnostics) 362: 363: =head3 MESSAGES 364: 365: | Code | Resolution | 366: |-------------------------|------------------------------------| 367: | warn_unsupported_obj | NWC object has no MusicXML mapping | 368: 369: =head3 FORMAL SPECIFICATION 370: 371: (placeholder) 372: 373: =cut 374: 375: sub warn_unsupported { 376: my ($self, %input) = @_; 377: my $args = validate_strict( 378: schema => { 379: file => { type => 'scalar' }, 380: staff => { type => 'scalar' }, 381: pos => { type => 'scalar', optional => 1, default => '?' }, 382: object => { type => 'scalar' }, 383: reason => { type => 'scalar', optional => 1, default => 'unknown' }, 384: }, 385: input => \%input, 386: ); 387: croak $@ unless defined $args; 388: 389: my $msg = _fmt_msg('warn_unsupported_obj', 390: $args->{file}, $args->{staff}, $args->{pos}, $args->{object}, $args->{reason}); 391: 392: $self->_record_warning($msg); 393: return $self;
Mutants (Total: 2, Killed: 2, Survived: 0)
394: } 395: 396: =head2 warn_approximate 397: 398: Record a warning that an NWC feature was approximated. 399: 400: =head3 Purpose 401: 402: Called when an NWC feature maps only approximately to a MusicXML construct. 403: 404: =head3 Arguments 405: 406: Named parameters: C<file>, C<staff>, C<pos>, C<feature>, C<approximation>. 407: 408: =head3 Returns 409: 410: C<$self>. 411: 412: =head3 API SPECIFICATION 413: 414: =head4 Input 415: 416: file : SCALAR (required) 417: staff : SCALAR (required) 418: pos : SCALAR (optional) 419: feature : SCALAR (required) 420: approximation : SCALAR (required) 421: 422: =head4 Output 423: 424: $self 425: 426: =head3 FORMAL SPECIFICATION 427: 428: (placeholder) 429: 430: =cut 431: 432: sub warn_approximate { 433: my ($self, %input) = @_; 434: my $args = validate_strict( 435: schema => { 436: file => { type => 'scalar' }, 437: staff => { type => 'scalar' }, 438: pos => { type => 'scalar', optional => 1, default => '?' }, 439: feature => { type => 'scalar' }, 440: approximation => { type => 'scalar' }, 441: }, 442: input => \%input, 443: ); 444: croak $@ unless defined $args; 445: 446: my $msg = _fmt_msg('warn_approx_feature', 447: $args->{file}, $args->{staff}, $args->{pos}, 448: $args->{feature}, $args->{approximation}); 449: 450: $self->_record_warning($msg); 451: return $self;
Mutants (Total: 2, Killed: 2, Survived: 0)
452: } 453: 454: =head2 count 455: 456: Update the batch summary counters. 457: 458: =head3 Purpose 459: 460: Called by the top-level converter to track per-file outcomes. 461: 462: =head3 Arguments 463: 464: Named parameter: C<outcome> -- one of C<processed>, C<successful>, 465: C<warnings>, C<failed>. 466: 467: =head3 Returns 468: 469: C<$self>. 470: 471: =head3 API SPECIFICATION 472: 473: =head4 Input 474: 475: outcome : SCALAR (required) 476: -- Valid domain (4 values only, case-sensitive): 477: -- 'processed', 'successful', 'warnings', 'failed' 478: -- Invalid: undef, '' (empty), wrong-case ('Processed'), any other 479: -- string -> croak error_internal 'Unknown counter: ...' 480: 481: =head4 Output 482: 483: $self 484: 485: =head3 FORMAL SPECIFICATION 486: 487: (placeholder) 488: 489: =cut 490: 491: sub count { 492: my ($self, %input) = @_; 493: my $args = validate_strict( 494: schema => { outcome => { type => 'scalar' } }, 495: input => \%input, 496: ); 497: croak $@ unless defined $args; 498: 499: croak _fmt_msg('error_internal', 'Unknown counter: ' . $args->{outcome}) 500: unless exists $self->{_counts}{ $args->{outcome} }; 501: 502: $self->{_counts}{ $args->{outcome} }++; 503: return $self;
Mutants (Total: 2, Killed: 2, Survived: 0)
504: } 505: 506: =head2 summary 507: 508: Print the batch processing summary. 509: 510: =head3 Purpose 511: 512: Emits the Files processed / Successful / Warnings / Failed summary to STDERR 513: at the end of a batch run. 514: 515: =head3 Returns 516: 517: C<$self>. 518: 519: =head3 API SPECIFICATION 520: 521: =head4 Input 522: 523: (none) 524: 525: =head4 Output 526: 527: $self 528: 529: =head3 FORMAL SPECIFICATION 530: 531: (placeholder) 532: 533: =cut 534: 535: sub summary { 536: my ($self) = @_; 537: return $self if $self->{_level} < $LOG_NORMAL;
538: my $c = $self->{_counts}; 539: print STDERR _fmt_msg('info_summary', 540: $c->{processed}, $c->{successful}, $c->{warnings}, $c->{failed}), "\n"; 541: return $self;Mutants (Total: 5, Killed: 0, Survived: 5)
- NUM_BOUNDARY_537_34_>: Numeric boundary flip < to >
HIGH: Likely missing edge-case test (boundary value)🧪 Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );- NUM_BOUNDARY_537_34_<=: Numeric boundary flip < to <=
HIGH: Likely missing edge-case test (boundary value)🧪 Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );- NUM_BOUNDARY_537_34_>=: Numeric boundary flip < to >=
HIGH: Likely missing edge-case test (boundary value)🧪 Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );- BOOL_NEGATE_537_2: Negate boolean return expression
MEDIUM: Add tests asserting both true and false outcomes🧪 Suggested Test# Boolean branch test suggestion ok( !func(INPUT), 'Verify boolean branch behaviour' );- RETURN_UNDEF_537_2: Replace return expression with undef
LOW: Mutation survived, but impact may be minor🧪 Suggested Test# Return value assertion is( func(INPUT), EXPECTED, 'Verify correct return value' );Mutants (Total: 2, Killed: 2, Survived: 0)
542: } 543: 544: =head2 warnings 545: 546: Return an arrayref of all collected warning strings. 547: 548: =head3 Returns 549: 550: Arrayref of strings. 551: 552: =head3 API SPECIFICATION 553: 554: =head4 Input 555: 556: (none) 557: 558: =head4 Output 559: 560: arrayref of SCALAR 561: 562: =cut 563: 564: sub warnings { 565: my ($self) = @_; 566: return $self->{_warnings};
Mutants (Total: 2, Killed: 2, Survived: 0)
567: } 568: 569: =head2 has_warnings 570: 571: Return true if any warnings have been collected. 572: 573: =cut 574: 575: sub has_warnings { 576: my ($self) = @_; 577: return scalar @{ $self->{_warnings} } > 0;
Mutants (Total: 5, Killed: 5, Survived: 0)
578: } 579: 580: # --------------------------------------------------------------------------- 581: # Private helpers 582: # --------------------------------------------------------------------------- 583: 584: sub _record_warning { ●585 → 588 → 591 585: my ($self, $msg) = @_; 586: push @{ $self->{_warnings} }, $msg; 587: $self->{_counts}{warnings}++; 588: if (defined $self->{_warnings_fh}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
589: print { $self->{_warnings_fh} } 'WARNING: ', $msg, "\n"; 590: } ●591 → 591 → 594 591: if ($self->{_level} >= $LOG_NORMAL) {
592: print STDERR 'WARNING: ', $msg, "\n"; 593: } 594: return; 595: } 596: 597: # Class-level helper; not a method so it can be called before construction. 598: sub _fmt_msg { 599: my ($key, @args) = @_; 600: croak "Unknown message key: $key" unless exists $MESSAGES{$key}; 601: return sprintf $MESSAGES{$key}, @args;Mutants (Total: 4, Killed: 0, Survived: 4)
- NUM_BOUNDARY_591_22_>: Numeric boundary flip >= to >
HIGH: Likely missing edge-case test (boundary value)🧪 Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );- NUM_BOUNDARY_591_22_<: Numeric boundary flip >= to <
HIGH: Likely missing edge-case test (boundary value)🧪 Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );- NUM_BOUNDARY_591_22_<=: Numeric boundary flip >= to <=
HIGH: Likely missing edge-case test (boundary value)🧪 Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );- COND_INV_591_2: Invert condition if to unless
MEDIUM: Add tests asserting both true and false outcomesMutants (Total: 2, Killed: 2, Survived: 0)
602: } 603: 604: 1; 605: 606: __END__ 607: 608: =head1 DIAGNOSTICS 609: 610: See C<%MESSAGES> hash in source for all message keys and their 611: C<sprintf>-compatible format strings. 612: 613: =head1 LIMITATIONS 614: 615: =over 4 616: 617: =item * Warning file is opened externally; this module does not open files itself. 618: 619: =item * Thread safety is not guaranteed. 620: 621: =back 622: 623: =head1 AUTHOR 624: 625: Nigel Horne C<< <nigel.horne@gmail.com> >> 626: 627: =head1 LICENSE 628: 629: This library is free software; you can redistribute it and/or modify it 630: under the same terms as Perl itself. 631: 632: =cut