lib/Music/NWC2MusicXML/Diagnostics.pm

Structural Coverage (Approximate)

TER1 (Statement): 98.78%
TER2 (Branch): 76.92%
TER3 (LCSAJ): 100.0% (2/2)
Approximate LCSAJ segments: 27

LCSAJ Legend

● 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.

Mutant Testing Legend

Survived (tests missed this) Killed (tests detected this) No mutation
    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;

Mutants (Total: 5, Killed: 2, Survived: 3)
212: print STDERR $message, "\n"; 213: return $self;

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;

Mutants (Total: 5, Killed: 2, Survived: 3)
256: print STDERR $message, "\n"; 257: return $self;

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;

Mutants (Total: 5, Killed: 2, Survived: 3)
299: print STDERR '[DEBUG] ', $message, "\n"; 300: return $self;

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;

Mutants (Total: 5, Killed: 0, Survived: 5)
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: 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) {

Mutants (Total: 4, Killed: 0, Survived: 4)
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: 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