test/parser

Standard Library source code

Parse TAP output and summarize test counts.

Module

Name
test/parser
Area
Standard Library
Source
modules/test/parser.zzm
=encoding utf8

=head1 NAME

test/parser - Parse TAP output and summarize test counts.

=head1 SYNOPSIS

  from test/parser import parse;

  let tap := "ok 1 - alpha\nnot ok 2 - beta\n1..2\n";
  let summary := parse( tap );

  # Top-level counts
  say( summary{top_level}{passed} );

  # All assertions, including nested subtests
  say( summary{assertions}{failed} );

  # Top-level plan, if present
  say( summary{planned} );

=head1 IMPLEMENTATION SUPPORT

This module is supported by all implementations of ZuzuScript.

=head1 DESCRIPTION

C<test/parser> parses lines in TAP (Test Anything Protocol) format and
returns a summary dictionary.

The summary tracks:

=over

=item * Top-level assertion outcomes.

A top-level assertion is any C<ok ...> or C<not ok ...> line with no
subtest indentation. A subtest block therefore counts as one
top-level test via the parent assertion line.

=item * Total assertion outcomes across all levels.

This includes assertions inside subtests, including nested subtests.

=item * Planned top-level test count.

This is the final number from a top-level C<1..N> plan line, or
C<null> if no top-level plan is present.

=item * Parsed test assertion rows.

Each parsed assertion is appended to C<tests>, including whether it is
at top level and its parsed number/description metadata.

=back

Outcome buckets are mutually exclusive and reported as C<passed>,
C<failed>, C<todo>, and C<skipped>. If an assertion line has a TODO or
SKIP directive, it is counted in that bucket rather than pass/fail.

=head1 EXPORTS

=head2 Functions

=over

=item C<< parse(String tap) >>

Parameters: C<tap> is TAP output text. Returns: C<Dict>. Parses a TAP
string and returns a summary dictionary.

=item C<< parse_lines(Array lines) >>

Parameters: C<lines> is an array of TAP lines. Returns: C<Dict>. Parses
TAP from already-split lines and returns the same summary dictionary as
C<parse>.

=back

=cut

from std/string import split, substr, trim;

function _new_counts () {
	return {
		passed: 0,
		failed: 0,
		todo: 0,
		skipped: 0,
		total: 0,
	};
}

function _strip_indent ( String line ) {
	let rest := line;
	while ( rest ~ /^    / ) {
		rest := substr( rest, 4 );
	}
	return rest;
}

function _bucket_for_assertion ( String line ) {
	if ( line ~ /#\s*todo\b/i ) {
		return "todo";
	}
	if ( line ~ /#\s*skip\b/i ) {
		return "skipped";
	}
	if ( line ~ /^ok\b/ ) {
		return "passed";
	}
	return "failed";
}

function _count_assertion ( Dict counts, String bucket ) {
	counts.set( "total", counts.get( "total", 0 ) + 1 );
	if ( bucket ≡ "passed" ) {
		counts.set( "passed", counts.get( "passed", 0 ) + 1 );
	}
	else if ( bucket ≡ "failed" ) {
		counts.set( "failed", counts.get( "failed", 0 ) + 1 );
	}
	else if ( bucket ≡ "todo" ) {
		counts.set( "todo", counts.get( "todo", 0 ) + 1 );
	}
	else if ( bucket ≡ "skipped" ) {
		counts.set( "skipped", counts.get( "skipped", 0 ) + 1 );
	}
}

function _parse_test_row ( String body, String bucket, Boolean is_top_level ) {
	let ok := body ~ /^ok\b/;
	let rest := ok ? substr( body, 3 ): substr( body, 7 );
	let number := null;
	let description := "";

	let parts := split( rest, " - ", 2 );
	if ( parts.length() > 0 ) {
		number := trim( parts [0] ) + 0;
	}
	if ( parts.length() > 1 ) {
		let desc_parts := split( parts [1], " #", 2 );
		description := trim( desc_parts [0] );
	}

	return {
		ok: ok,
		number: number,
		description: description,
		bucket: bucket,
		top_level: is_top_level,
		raw: body,
	};
}

function parse_lines ( Array lines ) {
	let top_level := _new_counts();
	let assertions := _new_counts();
	let planned := null;
	let tests := [];

	for ( let line in lines ) {
		if ( line ≡ "" ) {
			next;
		}

		let is_top_level := not ( line ~ /^    / );
		let body := _strip_indent( line );

		if ( body ~ /^\d+\.\.\d+\b/ ) {
			if ( is_top_level ) {
				let parts := split( body, "..", 2 );
				planned := parts.get( 1, "" ) + 0;
			}
			next;
		}

		if ( body ~ /^ok\b/ or body ~ /^not ok\b/ ) {
			let bucket := _bucket_for_assertion( body );
			_count_assertion( assertions, bucket );
			if ( is_top_level ) {
				_count_assertion( top_level, bucket );
			}
			tests.push( _parse_test_row( body, bucket, is_top_level ) );
		}
	}

	return {
		top_level: top_level,
		assertions: assertions,
		planned: planned,
		tests: tests,
	};
}

function parse ( String tap ) {
	return parse_lines( split( tap, /\r?\n/ ) );
}

=head1 COPYRIGHT AND LICENCE

B<< test/parser >> is copyright Toby Inkster.

It is free software; you may redistribute it and/or modify it under
the terms of either the Artistic License 1.0 or the GNU General Public
License version 2.

=cut