[igt-dev] [PATCH i-g-t 1/1] scripts: add a parser to produce documentation from Kernel C file metatags

Mauro Carvalho Chehab mauro.chehab at linux.intel.com
Fri Feb 3 08:26:50 UTC 2023


From: Mauro Carvalho Chehab <mchehab at kernel.org>

On a similar approach to Kernel's kernel-doc script, add a parser
that will produce documentation from special-purpose documentation
tags inside IGT tests.

Signed-off-by: Mauro Carvalho Chehab <mchehab at kernel.org>
---
 scripts/igt-doc | 647 ++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 647 insertions(+)
 create mode 100755 scripts/igt-doc

diff --git a/scripts/igt-doc b/scripts/igt-doc
new file mode 100755
index 000000000000..3ed2be144e61
--- /dev/null
+++ b/scripts/igt-doc
@@ -0,0 +1,647 @@
+#!/usr/bin/env perl
+# SPDX-License-Identifier: GPL-2.0
+
+## Copyright (C) 2023    Intel Corporation                 ##
+## Author: Mauro Carvalho Chehab <mchehab at kernel.org>      ##
+## Inspired on Linux kernel-doc script                     ##
+
+use warnings;
+use strict;
+use Getopt::Long;
+use Pod::Usage qw/pod2usage/;
+
+my $igt_build_path = "build/";
+my $igt_runner = "/runner/igt_runner";
+
+# Fields that mat be inside TEST and SUBTEST macros
+my @fields = (
+	# Hardware building block / Software building block / ...
+	"Category",
+
+	# waitfence / dmabuf/ sysfs / debugfs / ...
+	"Sub-category",
+
+	# functionality test / pereformance / stress
+	"Test type",
+
+	# BAT / workarouds / developer-specific / ...
+	"Run type",
+
+	# Bug tracker issue(s)
+	"Issues?",
+
+	# all / DG1 / DG2 / TGL / MTL / PVC / ATS-M / ...
+	"Platforms?",
+
+	# Any other specific platform requirements
+	"Platform requirements?",
+
+	# some other IGT test, like igt at test@subtest
+	"Depends on",
+
+	# other non-platform requirements
+	"Requirements?",
+
+	# Description of the test
+	"Description",
+);
+
+my %doc;
+my $min_test_prefix;
+
+my $out = "";
+
+sub print_subtest($$$)
+{
+	my $fname = shift;
+	my $test = shift;
+	my $key = shift;
+
+	my $name = $fname;
+	$name =~ s,.*tests/,,;
+	$name =~ s/.[ch]$//;
+
+	my $test_name = "igt\@" . $name;
+
+	my $summary = $test_name . "@" . $doc{$test}{"subtest"}{$key}{"Summary"};
+
+	return if (!$summary);
+
+	my $num_vars = $summary =~ tr/%//;
+
+	# Trivial case: no variables at the subtest
+	if ($num_vars == 0) {
+		printf "\n\n$summary\n";
+		printf '=' x length($summary);
+		printf "\n\n";
+
+		foreach my $k (sort keys %{$doc{$test}{"subtest"}{$key}}) {
+			next if ($k eq "Summary");
+			next if ($k eq "arg");
+
+			next if ($doc{$test}{"subtest"}{$key}{$k} eq $doc{$test}{$k});
+
+			printf ":$k: %s\n", $doc{$test}{"subtest"}{$key}{$k};
+		}
+		print "\n";
+
+		return;
+	}
+
+	# Convert arguments into an array
+	my @arg_array;
+	my $arg_ref = $doc{$test}{"subtest"}{$key}{"arg"};
+
+	foreach my $n (keys %{$arg_ref}) {
+		next if ($n >= $num_vars);
+		foreach my $el (sort keys %{$arg_ref->{$n}}) {
+			push @{$arg_array[$n]}, $el;
+		}
+	}
+
+	my $size = scalar @arg_array;
+
+	if ($size < $num_vars) {
+		printf STDERR
+			"$fname: subtest '%s' subtest needs %d arguments but only %d are defined.\n",
+			$summary, $num_vars, $size;
+		exit 1;
+	}
+
+	for (my $j = 0; $j < $num_vars; $j++) {
+		if (!defined($arg_array[$j][0])) {
+			printf STDERR
+				"$fname: subtest '%s' needs arg[%d], but this is not defined.\n",
+				$summary, $j+1;
+			exit 1;
+		}
+	}
+
+	# convert numeric wildcards to string ones
+	$summary =~ s/%(d|ld|lld|i|u|lu|llu)/%s/;
+
+	my @pos = map { 0 } 1..$num_vars;
+	my @args;
+	my @arg_map;
+
+	while (1) {
+		for (my $j = 0; $j < $num_vars; $j++) {
+			my $a = $arg_array[$j][$pos[$j]];
+			$args[$j] = $a;
+
+			if (defined($arg_ref->{$j}{$a})) {
+				$arg_map[$j] = $arg_ref->{$j}{$a};
+			} else {
+				$arg_map[$j] = $a;
+			}
+			if ($arg_ref->{$j}{$a} =~ m/\<.*\>/) {
+				$args[$j] = "<$a>";
+			}
+		}
+
+		my $arg_summary = sprintf $summary, @args;
+
+		# Output the elements
+
+		printf "\n\n$arg_summary\n";
+		printf '=' x length($arg_summary);
+		printf "\n\n";
+
+		foreach my $k (sort keys %{$doc{$test}{"subtest"}{$key}}) {
+			next if ($k eq "Summary");
+			next if ($k eq "arg");
+
+
+			my $str = $doc{$test}{"subtest"}{$key}{$k};
+
+			$str =~ s/\%?\barg\[(\d+)\]/$arg_map[$1 - 1]/g;
+
+			next if ($str eq $doc{$test}{$k});
+
+			printf ":$k: %s\n", $str;
+		}
+		print "\n";
+
+		# Increment variable inside the array
+
+		my $p = 0;
+		while (!defined(@{$arg_array[$p]}[$pos[$p] + 1])) {
+			$pos[$p] = 0;
+			$p++;
+		    last if ($p >= $num_vars);
+		}
+		last if ($p >= $num_vars);
+		$pos[$p]++;
+	}
+}
+
+sub print_test()
+{
+	foreach my $test (sort {$a <=> $b} keys %doc) {
+		my $fname = $doc{$test}{"File"};
+		my $name = $fname;
+
+		$name =~ s,.*tests/,,;
+		$name =~ s/.[ch]$//;
+
+		my $test_name = "igt\@" . $name;
+
+
+		print '=' x length($test_name);
+		print "\n$test_name\n";
+		print '=' x length($test_name);
+		print "\n\n";
+
+		foreach my $k (sort keys %{$doc{$test}}) {
+			next if ($k eq "subtest");
+
+			printf ":$k: %s\n", $doc{$test}{$k};
+		}
+
+		foreach my $key (sort {$a <=> $b} keys %{$doc{$test}{"subtest"}}) {
+			print_subtest($fname, $test, $key);
+		}
+	}
+}
+
+sub get_subtests()
+{
+	my @subtests;
+
+	foreach my $test (keys %doc) {
+		my $fname = $doc{$test}{"File"};
+		my $name = $fname;
+
+		$name =~ s,.*tests/,,;
+		$name =~ s/.[ch]$//;
+
+		my $test_name = "igt\@" . $name;
+
+		foreach my $key (keys %{$doc{$test}{"subtest"}}) {
+			my $summary = $test_name . "@" . $doc{$test}{"subtest"}{$key}{"Summary"};
+
+			next if (!$summary);
+
+			my $num_vars = $summary =~ tr/%//;
+
+			# Trivial case: no variables at the subtest
+			if ($num_vars == 0) {
+				push @subtests, $summary;
+			} else {
+				# Convert arguments into an array
+				my @arg_array;
+				my $arg_ref = $doc{$test}{"subtest"}{$key}{"arg"};
+
+				foreach my $n (keys %{$arg_ref}) {
+					next if ($n >= $num_vars);
+					foreach my $el (sort keys %{$arg_ref->{$n}}) {
+						push @{$arg_array[$n]}, $el;
+					}
+				}
+
+				my $size = scalar @arg_array;
+
+				if ($size < $num_vars) {
+					printf STDERR
+					       "$fname: subtest '%s' subtest needs %d arguments but only %d are defined.\n",
+					       $summary, $num_vars, $size;
+					exit 1;
+				}
+
+				for (my $j = 0; $j < $num_vars; $j++) {
+					if (!defined($arg_array[$j][0])) {
+						printf STDERR
+						       "$fname: subtest '%s' needs arg[%d], but this is not defined.\n",
+						       $summary, $j+1;
+						exit 1;
+					}
+				}
+
+				# convert numeric wildcards to string ones
+				$summary =~ s/%(d|ld|lld|i|u|lu|llu)/%s/;
+				my @pos = map { 0 } 1..$num_vars;
+				my @args;
+
+				while (1) {
+					for (my $j = 0; $j < $num_vars; $j++) {
+						my $a = $arg_array[$j][$pos[$j]];
+						$args[$j] = $a;
+						if ($arg_ref->{$j}{$a} =~ m/\<.*\>/) {
+							$args[$j] = "<$a>";
+						}
+					}
+
+					my $arg_summary = sprintf $summary, @args;
+
+					push @subtests, $arg_summary;
+
+					# Increment variable inside the array
+
+					my $p = 0;
+					while (!defined(@{$arg_array[$p]}[$pos[$p] + 1])) {
+						$pos[$p] = 0;
+						$p++;
+					    last if ($p >= $num_vars);
+					}
+					last if ($p >= $num_vars);
+					$pos[$p]++;
+				}
+			}
+		}
+	}
+
+	return @subtests;
+}
+
+sub check_testlist()
+{
+	my @doc_subtests = get_subtests();
+
+	# Get a list of tests from
+	my @run_subtests;
+
+	open TESTLIST, "$igt_build_path/$igt_runner -L -t '$min_test_prefix' $igt_build_path/tests |" or die "Can't run igt_runner";
+	while (<TESTLIST>) {
+		s/\n//;
+		push @run_subtests, $_;
+	}
+	close TESTLIST;
+
+	# Compare arrays
+
+	my @run_missing;
+	my @doc_uneeded;
+
+	foreach my $t1 (@doc_subtests) {
+		my $re = $t1;
+		$re =~ s,\<[^\>]+\>,\\d+,g;
+
+		my $match = 0;
+		foreach my $t2 (sort @run_subtests) {
+			if ($t2 =~ m/^$re$/) {
+				$match = 1;
+			}
+		}
+		push @doc_uneeded, $t1 if (!$match);
+	}
+
+	foreach my $t2 (sort @run_subtests) {
+		my $found = 0;
+		foreach my $t1 (@doc_subtests) {
+			my $re = $t1;
+			$re =~ s,\<[^\>]+\>,\\d+,g;
+			if ($t2 =~ m/^$re$/) {
+				$found = 1;
+			}
+		}
+		push @run_missing, $t2 if (!$found);
+	}
+
+	if (@doc_uneeded) {
+		printf "Unused documentation:\n";
+		foreach my $t (@doc_uneeded) {
+			print "\t$t\n";
+		}
+	}
+
+	if (@run_missing) {
+		printf "Missing documentation:\n";
+		foreach my $t (@run_missing) {
+			print "\t$t\n";
+		}
+	}
+}
+
+
+
+#
+# Main: parse the files and fill %doc struct
+#
+
+my $rest;
+my $show_subtests;
+my $check_testlist;
+my $help;
+my $man;
+
+GetOptions(
+	"show-subtests" => \$show_subtests,
+	"igt-build-path" => \$igt_build_path,
+	"check-testlist" => \$check_testlist,
+	"rest|rst" => \$rest,
+	'help|?' => \$help,
+	man => \$man
+) or pod2usage(2);
+
+pod2usage(1) if $help;
+pod2usage(-exitstatus => 0, -verbose => 2) if $man;
+
+my $current_test;
+my $current_subtest;
+
+my $handle_section = "";
+my $current_field = "";
+my $cur_arg = -1;
+my $cur_arg_element = 0;
+my $test_number = 0;
+
+my  $field_re = join '|', @fields;
+
+while ($ARGV[0]) {
+	my $fname = $ARGV[0];
+	shift @ARGV;
+
+	# Dynamically build a test prefix from the file name
+
+	my $prefix = $fname;
+
+	$prefix =~ s,.*tests/,,;
+	$prefix =~ s,(.*/).*,$1,;
+	$prefix = "igt\@" . $prefix;
+
+	if (!$min_test_prefix) {
+		$min_test_prefix = $prefix;
+	} elsif (length($prefix) < length($min_test_prefix)) {
+		$min_test_prefix = $prefix;
+	}
+
+	open IN, $fname or die "Can't open $fname";
+
+	my $arg_ref;
+
+	$current_test = "";
+	my $subtest_number = 0;
+
+	while (<IN>) {
+		my $ln = $_;
+
+		next if ($ln =~ m/^\s*\*$/);
+
+		if ($ln =~ m,^\s*\*/$,) {
+			$handle_section = "";
+			undef($current_subtest);
+			undef($arg_ref);
+			$cur_arg = -1;
+
+			next;
+		}
+
+		if ($ln =~ m,^\s*/\*\*$,) {
+			$handle_section = "1";
+			next;
+		}
+
+		next if (!$handle_section);
+
+		$ln =~ s/^\s*\*\s*//;
+
+		# Handle only known sections
+		if ($handle_section eq "1") {
+			$current_field = "";
+			if ($ln =~ m/^TEST:\s*(.*)/) {
+				$current_test = $test_number;
+				$handle_section = "test";
+				$test_number++;
+
+				$doc{$current_test} = ();
+				$doc{$current_test}{"Summary"} = $1;
+				$doc{$current_test}{"File"} = $fname;
+				undef($current_subtest);
+
+				next;
+			}
+		}
+
+		if ($ln =~ m/^SUBTESTS?:\s*(.*)/) {
+			$current_field = "";
+			$handle_section = "subtest";
+			$current_subtest = $subtest_number++;
+
+			$doc{$current_test}{"subtest"}{$current_subtest} = ();
+
+			$doc{$current_test}{"subtest"}{$current_subtest}{"Summary"} = $1;
+			$doc{$current_test}{"subtest"}{$current_subtest}{"Description"} = "";
+
+			if (!defined($arg_ref)) {
+				my %new_arg = ();
+
+				$arg_ref = \%new_arg;
+			}
+			$doc{$current_test}{"subtest"}{$current_subtest}{"arg"} = $arg_ref;
+		}
+
+		# It is a known section. Parse its contents
+
+		if ($ln =~ m/($field_re):\s*(.*)/i) {
+			$current_field = lc $1;
+			my $v = $2;
+
+			$current_field =~ s/^([a-z])/\U$1/;
+			if ($handle_section eq "test") {
+				$doc{$current_test}{$current_field} = $v;
+			} else {
+				$doc{$current_test}{"subtest"}{$current_subtest}{$current_field} = $v;
+			}
+
+			$cur_arg = -1;
+			next;
+		}
+
+		# Use hashes for arguments to avoid duplication
+		if ($ln =~ m/arg\[(\d+)\]:\s*(.*)/) {
+			$current_field = "";
+			if (!defined($arg_ref)) {
+				die "$fname:$.: arguments should be defined after one or more subtests, at the same comment\n";
+			}
+
+			$cur_arg = $1 - 1;
+			$cur_arg_element = $2;
+
+			# Should be used only for numeric values
+			$arg_ref->{$cur_arg}{$cur_arg_element} = "<$2>" if ($2);
+			next;
+		}
+		if ($cur_arg >= 0 && $ln =~ m/\@(\S+):\s*(.*)/) {
+			$current_field = "";
+			if (!defined($arg_ref)) {
+				die "$fname:$.: arguments should be defined after one or more subtests, at the same comment\n";
+			}
+
+			$cur_arg_element = $1;
+			$arg_ref->{$cur_arg}{$cur_arg_element} = $2;
+			next;
+		}
+		if ($ln =~ m/\@(\S+):\s*(.*)/) {
+			$current_field = "";
+			if (!defined($arg_ref)) {
+				die "$fname:$.: arguments should be defined after one or more subtests, at the same comment\n";
+			}
+
+			printf "$fname:$.: Warning: invalid argument: \@$1: $2\n";
+		}
+
+		# Handle multi-line field contents
+		if ($current_field) {
+			if (m/\s*\*?\s*(.*)/) {
+				if ($handle_section eq "test") {
+					$doc{$current_test}{$current_field} .= " $1";
+				} else {
+					$doc{$current_test}{"subtest"}{$current_subtest}{$current_field} .= " $1";
+				}
+			}
+		}
+
+		# Handle multi-line argument contents
+		if ($cur_arg >= 0) {
+			if (m/\s*\*?\s*(.*)/) {
+				my $v = $1;
+				if ($arg_ref->{$cur_arg}{$cur_arg_element} =~ m/(.*)>$/) {
+					$arg_ref->{$cur_arg}{$cur_arg_element} = "$1 $v>";
+				} else {
+					$arg_ref->{$cur_arg}{$cur_arg_element} .= " $v";
+				}
+			}
+		}
+	}
+	close IN;
+}
+
+# Now all files were read, do some output
+my $run = 0;
+
+if ($show_subtests) {
+	$run = 1;
+	my @subtests = get_subtests();
+
+	for my $subtest (sort @subtests) {
+		print "$subtest\n";
+	}
+}
+
+if ($check_testlist) {
+	$run = 1;
+
+	check_testlist;
+}
+
+if (!$run || $rest) {
+	print_test;
+}
+
+# Script documentation
+
+__END__
+
+=head1 NAME
+
+igt-doc - Print formatted kernel documentation to stdout
+
+=head1 SYNOPSIS
+
+ igt-doc [options] FILES
+
+ Where [options] can be:
+
+	--rest			- Produces documentation in ReST format
+	--show-subtests		- Shows a list of documented subtests
+	--check-testlist	- Audit if the documentation is aligned with
+				  the sources
+	--man			- Shows a man-style page
+	--help			- Shows help
+
+=head1 DESCRIPTION
+
+This script looks for special documentation markups inside the IGT
+source code and produces documents in ReST format from them.
+
+=head1 OPTIONS
+
+=over 8
+
+=item B<--rest>
+
+Generate documentation from the source files, in ReST file format.
+
+Called by default if no other output mode parameter is used.
+
+=item B<--show-subtests>
+
+Shows the name of the documented subtests in alphabetical order.
+
+=item B<--igt-build-path>
+
+Path where the IGT runner is sitting. Used by B<--check-testlist>.
+
+Default: build/
+
+=item B<--check-testlist>
+
+Check the documentation against the testlist as identified by IGT runner,
+identifying documentation gaps.
+
+=item B<--help>
+
+Displays a help message.
+
+=item B<--man>
+
+Displays a man-style message.
+
+=back
+
+=head1 BUGS
+
+Report bugs to Mauro Carvalho Chehab <mchehab at kernel.org>
+
+=head1 COPYRIGHT
+
+Authored 2023 by Mauro Carvalho Chehab <mchehab at kernel.org>
+
+Copyright (c) 2023 Intel Corporation
+
+License GPLv2: GNU GPL version 2 <http://gnu.org/licenses/gpl.html>.
+
+This is free software: you are free to change and redistribute it.
+There is NO WARRANTY, to the extent permitted by law.
+
+=cut
-- 
2.39.0



More information about the igt-dev mailing list