#!/usr/bin/perl
#
# make_tests.pl - generate WPT test cases from the testable statements wiki
#
# This script assumes that a wiki has testable statement entries
# in the format described by the specification at
# https://spec-ops.github.io/atta-api/index.html
#
# usage: make_tests.pl -f file | -w wiki_title | -s spec -d dir
use strict;
use IO::String ;
use JSON ;
use MediaWiki::API ;
use Getopt::Long;
my %specs = (
"aria11" => {
title => "ARIA_1.1_Testable_Statements",
specURL => "https://www.w3.org/TR/wai-aria11/",
dir => "aria11"
},
"svg" => {
title => "SVG_Accessibility/Testing/Test_Assertions_with_Tables_for_ATTA",
specURL => "https://www.w3.org/TR/svg-aam-1.0/",
dir => "svg",
fragment => '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">%code%</svg>'
}
);
my @apiNames = qw(UIA MSAA ATK IAccessible2 AXAPI);
my $apiNamesRegex = "(" . join("|", @apiNames) . ")";
# the suffix to attach to the automatically generated test case names
my $theSuffix = "-manual.html";
# dir is determined based upon the short name of the spec and is defined
# by the input or on the command line
my $file = undef ;
my $spec = undef ;
my $wiki_title = undef ;
my $dir = undef;
my $theSpecFragment = "%code%";
my $preserveWiki = "";
my $fake = 0;
my $result = GetOptions(
"f|file=s" => \$file,
"p=s" => \$preserveWiki,
"w|wiki=s" => \$wiki_title,
"s|spec=s" => \$spec,
"f|fake" => \$fake,
"d|dir=s" => \$dir) || usage();
my $wiki_config = {
"api_url" => "https://www.w3.org/wiki/api.php"
};
my $io ;
our $theSpecURL = "";
if ($spec) {
print "Processing spec $spec\n";
$wiki_title = $specs{$spec}->{title};
$theSpecURL = $specs{$spec}->{specURL};
if (!$dir) {
$dir = "../" . $specs{$spec}->{dir};
}
$theSpecFragment = $specs{$spec}->{fragment};
}
if (!$dir) {
$dir = "../raw";
}
if (!-d $dir) {
print STDERR "No such directory: $dir\n";
exit 1;
}
if ($file) {
open($io, "<", $file) || die("Failed to open $file: " . $@);
} elsif ($wiki_title) {
my $MW = MediaWiki::API->new( $wiki_config );
$MW->{config}->{on_error} = \&on_error;
sub on_error {
print "Error code: " . $MW->{error}->{code} . "\n";
print $MW->{error}->{stacktrace}."\n";
die;
}
my $page = $MW->get_page( { title => $wiki_title } );
my $theContent = $page->{'*'};
print "Loaded " . length($theContent) . " from $wiki_title\n";
if ($preserveWiki) {
if (open(OUTPUT, ">$preserveWiki")) {
print OUTPUT $theContent;
close OUTPUT;
print "Wiki preserved in $preserveWiki\n";
exit 0;
} else {
print "Failed to create $preserveWiki. Terminating.\n";
exit 1;
}
}
$io = IO::String->new($theContent);
} else {
usage() ;
}
# Now let's walk through the content and build a test page for every item
#
# iterate over the content
# my $io ;
# open($io, "<", "raw") ;
# data structure:
#
# steps is a list of steps to be performed.
# Each step is an object that has a type property and other properties based upon that type.
#
# Types include:
#
# 'test' - has a property for each ATAPI for which there are tests
# 'attribute' - has a property for the target id, attribute name, and value
# 'event' - has a property for the target id and event name
my $state = 0; # between items
my $theStep = undef;
my $current = "";
my $theCode = "";
my $theAttributes = {};
my @steps ;
my $theAsserts = {} ;
my $theAssertCount = 0;
my $theAPI = "";
my $typeRows = 0;
my $theType = "";
my $theName = "";
my $theRef = "";
my $lineCounter = 0;
my $skipping = 0;
our $testNames = {} ;
while (<$io>) {
if (m/<!-- END OF TESTS -->/) {
last;
}
$lineCounter++;
# look for state
if (m/^SpecURL: (.*)$/) {
$theSpecURL = $1;
$theSpecURL =~ s/^ *//;
$theSpecURL =~ s/ *$//;
}
if ($state == 5 && m/^; \/\/ (.*)/) {
# we found another test inside a block
# we were in an item; dump it
build_test($current, $theAttributes, $theCode, \@steps, $theSpecFragment) ;
# print "Finished $current and new subblock $1\n";
$state = 1;
$theAttributes = {} ;
$theAPI = "";
@steps = ();
$theCode = "";
$theAsserts = undef;
$theName = "";
} elsif (m/^=== +(.*[^ ]) +===/) {
if ($state != 0) {
if ($skipping) {
print STDERR "Flag on assertion $current; skipping\n";
} else {
# we were in an item; dump it
build_test($current, $theAttributes, $theCode, \@steps, $theSpecFragment) ;
# print "Finished $current\n";
}
}
$state = 1;
$current = $1;
$theAttributes = {} ;
@steps = ();
$theCode = "";
$theAsserts = undef;
$theAPI = "";
$theName = "";
if ($current =~ m/\(/) {
# there is a paren in the name -skip it
$skipping = 1;
} else {
$skipping = 0;
}
}
if ($state == 1) {
if (m/<pre>/) {
# we are now in the code block
$state = 2;
next;
} elsif (m/==== +(.*) +====/) {
# we are in some other block
$theName = lc($1);
$theAttributes->{$theName} = "";
next;
}
if (m/^Reference: +(.*)$/) {
$theAttributes->{reference} = $theSpecURL . "#" . $1;
} elsif ($theName ne "") {
# accumulate whatever was in the block under the data for it
chomp();
$theAttributes->{$theName} .= $_;
} elsif (m/TODO/) {
$state = 0;
}
}
if ($state == 2) {
if (m/<\/pre>/) {
# we are done with the code block
$state = 3;
} else {
if (m/^\s/ && !m/if given/) {
# trim any trailing whitespace
$theCode =~ s/ +$//;
$theCode =~ s/\t/ /g;
$theCode .= $_;
# In MediaWiki, to display & symbol escapes as literal text, one
# must use "&&" for the "&" character. We need to undo that.
$theCode =~ s/&(\S)/&$1/g;
}
}
} elsif ($state == 3) {
# look for a table
if (m/^\{\|/) {
# table started
$state = 4;
}
} elsif ($state == 4) {
if (m/^\|-/) {
if ($theAPI
&& exists($theAsserts->{$theAPI}->[$theAssertCount])
&& scalar(@{$theAsserts->{$theAPI}->[$theAssertCount]})) {
$theAssertCount++;
}
# start of a table row
if ($theType ne "" && $typeRows) {
# print qq($theType typeRows was $typeRows\n);
# we are still processing items for a type
$typeRows--;
# populate the first cell
$theAsserts->{$theAPI}->[$theAssertCount] = [ $theType ] ;
} else {
$theType = "";
}
} elsif (m/^\|\}/) {
# ran out of table
$state = 5;
# adding processing for additional block types
# a colspan followed by a keyword triggers a start
# so |colspan=5|element triggers a new collection
# |colspan=5|attribute triggers the setting of an attribute
} elsif (m/^\|colspan="*([0-9])"*\|([^ ]+) (.*)$/) {
my $type = $2;
my $params = $3;
my $obj = {} ;
if ($type eq "attribute") {
if ($params =~ m/([^:]+):([^ ]+) +(.*)$/) {
$obj = {
type => $type,
element => $1,
attribute => $2,
value => $3
};
$theStep = undef;
push(@steps, $obj);
} else {
print STDERR "Malformed attribute instruction at line $lineCounter: " . $_ . "\n";
}
} elsif ($type eq "event") {
if ($params =~ m/([^:]+):([^ ]+).*$/) {
$obj = {
type => $type,
element => $1,
value => $2
};
$theStep = undef;
push(@steps, $obj);
} else {
print STDERR "Malformed event instruction at line $lineCounter: " . $_ . "\n";
}
} elsif ($type eq "element") {
$obj = {
type => "test",
element => $3
};
push(@steps, $obj);
$theStep = scalar(@steps) - 1;
$theAsserts = $steps[$theStep];
} else {
print STDERR "Unknown operation type: $type at line " . $lineCounter . "; skipping.\n";
}
} elsif (m/($apiNamesRegex)$/) {
my $theString = $1;
$theString =~ s/ +$//;
$theString =~ s/^ +//;
if ($theString eq "IA2") {
$theString = "IAccessible2" ;
}
my $rows = 1;
if (m/^\|rowspan="*([0-9])"*\|(.*)$/) {
$rows = $1
}
if (grep { $_ eq $theString } @apiNames) {
# we found an API name - were we already processing assertions?
if (!$theAsserts) {
# nope - now what?
$theAsserts = {
type => "test",
element => "test"
};
push(@steps, $theAsserts);
}
$theAssertCount = 0;
# this is a new API section
$theAPI = $theString ;
$theAsserts->{$theAPI} = [ [] ] ;
$theType = "";
} else {
# this is a multi-row type
$theType = $theString;
$typeRows = $rows;
# print qq(Found multi-row $theString for $theAPI with $typeRows rows\n);
$typeRows--;
# populate the first cell
if ($theAPI
&& exists($theAsserts->{$theAPI}->[$theAssertCount])
&& scalar(@{$theAsserts->{$theAPI}->[$theAssertCount]})) {
$theAssertCount++;
}
$theAsserts->{$theAPI}->[$theAssertCount] = [ $theType ] ;
}
} elsif (m/^\|(.*)$/) {
my $item = $1;
$item =~ s/^ *//;
$item =~ s/ *$//;
$item =~ s/^['"]//;
$item =~ s/['"]$//;
# add into the data structure for the API
if (!exists $theAsserts->{$theAPI}->[$theAssertCount]) {
$theAsserts->{$theAPI}->[$theAssertCount] = [ $item ] ;
} else {
push(@{$theAsserts->{$theAPI}->[$theAssertCount]}, $item);
}
}
}
};
if ($state != 0) {
build_test($current, $theAttributes, $theCode, \@steps, $theSpecFragment) ;
print "Finished $current\n";
}
exit 0;
# build_test
#
# create a test file
#
# attempts to create unique test names
sub build_test() {
my $title = shift ;
my $attrs = shift ;
my $code = shift ;
my $steps = shift;
my $frag = shift ;
if ($title eq "") {
print STDERR "No name provided!";
return;
}
if ($frag ne "") {
$frag =~ s/%code%/$code/;
$code = $frag;
}
$code =~ s/ +$//m;
$code =~ s/\t/ /g;
my $title_reference = $title;
if ($code eq "") {
print STDERR "No code for $title; skipping.\n";
return;
}
if ( $steps eq {}) {
print STDERR "No assertions for $title; skipping.\n";
return;
}
my $testDef =
{ "title" => $title,
"steps" => []
};
my $stepCount = 0;
foreach my $asserts (@$steps) {
$stepCount++;
my $step =
{
"type" => $asserts->{"type"},
"title"=> "step " . $stepCount,
};
if ($asserts->{type} eq "test") {
# everything in the block is about testing an element
$step->{"element"} = ( $asserts->{"element"} || "test" );
my $tests = {};
if ($fake) {
$tests->{"WAIFAKE"} = [ [ "property", "role", "is", "ROLE_TABLE_CELL" ], [ "property", "interfaces", "contains", "TableCell" ] ];
}
foreach my $name (@apiNames) {
if (exists $asserts->{$name} && scalar(@{$asserts->{$name}})) {
$tests->{$name} = $asserts->{$name};
}
};
$step->{test} = $tests;
} elsif ($asserts->{type} eq "attribute") {
$step->{type} = "attribute";
$step->{element} = $asserts->{"element"};
$step->{attribute} = $asserts->{"attribute"};
$step->{value} = $asserts->{value};
} elsif ($asserts->{type} eq "event") {
$step->{type} = "event";
$step->{element} = $asserts->{"element"};
$step->{event} = $asserts->{value};
} else {
print STDERR "Invalid step type: " . $asserts->{type} . "\n";
next;
}
push(@{$testDef->{steps}}, $step);
}
# populate the rest of the test definition
if (scalar(keys(%$attrs))) {
while (my $key = each(%$attrs)) {
# print "Copying $key \n";
$testDef->{$key} = $attrs->{$key};
}
}
if (exists $attrs->{reference}) {
$title_reference = "<a href='" . $attrs->{reference} . "'>" . $title_reference . "</a>" ;
}
my $testDef_json = to_json($testDef, { canonical => 1, pretty => 1, utf8 => 1});
my $fileName = $title;
$fileName =~ s/\s*$//;
$fileName =~ s/\///g;
$fileName =~ s/\s+/_/g;
$fileName =~ s/[,=:]/_/g;
$fileName =~ s/['"]//g;
my $count = 2;
if ($testNames->{$fileName}) {
while (exists $testNames->{$fileName . "_$count"}) {
$count++;
}
$fileName .= "_$count";
}
$fileName = lc($fileName);
$testNames->{$fileName} = 1;
$fileName .= $theSuffix;
my $template = qq(<!doctype html>
<html>
<head>
<title>$title</title>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type"/>
<link rel="stylesheet" href="/wai-aria/scripts/manual.css">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/wai-aria/scripts/ATTAcomm.js"></script>
<script>
setup({explicit_timeout: true, explicit_done: true });
var theTest = new ATTAcomm(
$testDef_json
) ;
</script>
</head>
<body>
<p>This test examines the ARIA properties for $title_reference.</p>
$code
<div id="manualMode"></div>
<div id="log"></div>
<div id="ATTAmessages"></div>
</body>
</html>);
my $file ;
if (open($file, ">", "$dir/$fileName")) {
print $file $template;
print $file "\n";
close $file;
} else {
print STDERR qq(Failed to create file "$dir/$fileName" $!\n);
}
return;
}
sub usage() {
print STDERR q(usage: make_tests.pl -f file | -w wiki_title | -s spec [-n -v -d dir ]
-s specname - the name of a spec known to the system
-w wiki_title - the TITLE of a wiki page with testable statements
-f file - the file from which to read
-n - do nothing
-v - be verbose
-d dir - put generated tests in directory dir
);
exit 1;
}
# vim: ts=2 sw=2 ai: