Skip to content

Commit f5b94e4

Browse files
committed
parser: Add KTAP parser for handling kernel selftests
This patch introduces a dedicated parser for Linux kernel selftests using the KTAP (Kernel Test Anything Protocol) format. While KTAP is based on the TAP (Test Anything Protocol) standard, it includes significant extensions tailored for kernel testing. This parser builds on TAP::Parser to handle common TAP elements while providing additional logic to interpret KTAP-specific constructs such as grouped subtests and prefixed comment lines. For more details on KTAP, see: https://docs.kernel.org/dev-tools/ktap.html Example output of the ktap test run: TAP version 13 1..13 # overriding timeout to 300 # selftests: cgroup: test_core # ok 1 test_cgcore_internal_process_constraint # ok 2 test_cgcore_top_down_constraint_enable # ok 3 test_cgcore_top_down_constraint_disable # ok 4 test_cgcore_no_internal_process_constraint_on_threads # ok 5 test_cgcore_parent_becomes_threaded # ok 6 test_cgcore_invalid_domain # ok 7 test_cgcore_populated # ok 8 test_cgcore_proc_migration # ok 9 test_cgcore_thread_migration # ok 10 test_cgcore_destroy # ok 11 test_cgcore_lesser_euid_open # ok 12 test_cgcore_lesser_ns_open ok 1 selftests: cgroup: test_core There are group of tests with nested subtests. Each group starts as follow: # selftests: cgroup: test_cpu After that individual tests are listed with results ok | not ok: # ok 1 test_cgcore_internal_process_constraint however such subtests are prepended with # which - from the TAP specification standpoint - are just comments. Moreover KTAP allows for more comments like: # overriding timeout to 300 Each group of tests ends with: ok 2 selftests: cgroup: test_cpu or not ok 10 selftests: cgroup: test_zswap # exit=1 The KTAP parser uses TAP::Parser to re-use all generic TAP functions however extends those to handle KTAP specifics. The KTAP parser correctly identifies these groups and subtests, associates results appropriately, and exports them in a structured format compatible with OpenQA.
1 parent 606fa3c commit f5b94e4

File tree

1 file changed

+213
-0
lines changed

1 file changed

+213
-0
lines changed

lib/OpenQA/Parser/Format/KTAP.pm

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
# Copyright 2025 SUSE LLC
2+
# SPDX-License-Identifier: GPL-2.0-or-later
3+
4+
package OpenQA::Parser::Format::KTAP;
5+
use Mojo::Base 'OpenQA::Parser::Format::Base';
6+
7+
# Translates KTAP kernel selftests format -> OpenQA internal representation
8+
use Carp qw(croak confess);
9+
use OpenQA::Parser::Result::OpenQA;
10+
use TAP::Parser;
11+
12+
has [qw(test steps)];
13+
14+
sub _is_tap {
15+
my ($raw) = @_;
16+
my $tap = TAP::Parser->new({ tap => $raw });
17+
confess 'KTAP parse error: ' . $tap->parse_errors if $tap->parse_errors;
18+
19+
while (my $result = $tap->next) {
20+
return $result->version if $result->type eq 'version';
21+
}
22+
23+
return undef;
24+
}
25+
26+
sub _testgroup_init {
27+
my ($self, $line, $test_ref, $steps_ref, $m_ref) = @_;
28+
29+
if ($$steps_ref && $self->test) {
30+
$$steps_ref->{name} = $self->test->{name};
31+
$$steps_ref->{test} = $self->test;
32+
$self->generated_tests_results->add(OpenQA::Parser::Result::OpenQA->new($$steps_ref));
33+
}
34+
35+
my ($group_name) = $line =~ /^#\s*selftests:\s+(.*)/;
36+
my $sanitized_group_name = "selftests: $group_name";
37+
$sanitized_group_name =~ s/[\/.]/_/g;
38+
39+
$$test_ref = {
40+
flags => {},
41+
category => 'KTAP',
42+
name => $sanitized_group_name,
43+
};
44+
45+
$self->test(OpenQA::Parser::Result->new($$test_ref));
46+
$self->_add_test($self->test);
47+
48+
$$steps_ref = OpenQA::Parser::Result->new({
49+
details => [],
50+
dents => 0,
51+
result => 'passed'
52+
});
53+
54+
$$m_ref = 0;
55+
}
56+
57+
sub _parse_subtest {
58+
my ($self, $result, $test_ref, $steps_ref, $m_ref) = @_;
59+
return unless $$steps_ref && $self->test;
60+
61+
my $line = $result->as_string;
62+
return unless $line =~ /^#\s*(ok|not ok)\s+\d+\s+(.+)/;
63+
64+
my ($status, $subtest_name) = ($1, $2);
65+
my $filename = "KTAP-@{[$$test_ref->{name}]}-$$m_ref.txt";
66+
67+
push @{ $$steps_ref->{details} }, {
68+
text => $filename,
69+
title => $subtest_name,
70+
result => $status eq 'ok' ? 'ok' : 'fail',
71+
};
72+
73+
$$steps_ref->{result} = 'fail' if $status ne 'ok';
74+
75+
$self->_add_output({file => $filename, content => $line});
76+
$$m_ref++;
77+
}
78+
79+
sub _testgroup_finalize {
80+
my ($self, $steps_ref, $result) = @_;
81+
return unless $$steps_ref && $self->test;
82+
83+
my $line = $result ? $result->as_string : '';
84+
my $group_failed = 0; #it is possible the subtests do not report 'not ok' but only the group
85+
86+
if ($line =~ /^not ok\b/i) {
87+
$$steps_ref->{result} = 'fail';
88+
$group_failed = 1;
89+
}
90+
elsif ($line =~ /#\s*SKIP\b/i) {
91+
$$steps_ref->{result} = 'skip';
92+
}
93+
94+
$$steps_ref->{name} = $self->test->{name};
95+
$$steps_ref->{test} = $self->test;
96+
97+
$self->generated_tests_results->add(OpenQA::Parser::Result::OpenQA->new($$steps_ref));
98+
99+
#clear the group
100+
$self->test(undef);
101+
#clewr the subtests
102+
$$steps_ref = undef;
103+
}
104+
105+
sub parse {
106+
my ($self, $KTAP) = @_;
107+
confess 'No KTAP given/loaded' unless $KTAP;
108+
109+
_is_tap($KTAP);
110+
111+
my $parser = TAP::Parser->new({ tap => $KTAP });
112+
my ($test, $steps, $m) = ({}, undef, 0);
113+
114+
while (my $result = $parser->next) {
115+
116+
next if $result->type eq 'version' || $result->type eq 'plan';
117+
118+
if ($result->type eq 'comment' && $result->as_string =~ /^#\s*selftests:\s+(.*)/) {
119+
$self->_testgroup_init($result->as_string, \$test, \$steps, \$m);
120+
next;
121+
}
122+
123+
if ($result->type eq 'comment' && $result->as_string =~ /^#\s*(ok|not ok)\s+\d+\s+(.+)/) {
124+
$self->_parse_subtest($result, \$test, \$steps, \$m);
125+
next;
126+
}
127+
128+
if ($result->type eq 'test' && $result->description =~ /^selftests: /) {
129+
$self->_testgroup_finalize(\$steps, $result);
130+
next;
131+
}
132+
}
133+
134+
if ($steps && $self->test) {
135+
$steps->{name} = $self->test->{name};
136+
$steps->{test} = $self->test;
137+
$self->generated_tests_results->add(OpenQA::Parser::Result::OpenQA->new($steps));
138+
}
139+
}
140+
141+
=head1 NAME
142+
143+
OpenQA::Parser::Format::KTAP - KTAP file parser
144+
145+
=head1 SYNOPSIS
146+
147+
use OpenQA::Parser::Format::KTAP;
148+
149+
my $parser = OpenQA::Parser::Format::KTAP->new()->load('test.tap');
150+
151+
# Alternative interface
152+
use OpenQA::Parser qw(parser p);
153+
154+
my $parser = p( KTAP => 'test.tap' );
155+
156+
my $result_collection = $parser->results();
157+
my $test_collection = $parser->tests();
158+
my $output_collection = $parser->output();
159+
160+
my $arrayref = $result_collection->to_array;
161+
162+
$parser->results->remove(0);
163+
164+
my $passed_results = $parser->results->search( result => qr/ok/ );
165+
my $size = $passed_results->size;
166+
167+
=head1 DESCRIPTION
168+
169+
B<OpenQA::Parser::Format::KTAP> parses Linux kernel selftests written in KTAP (Kernel Test Anything Protocol),
170+
which extends TAP with structured subtests and test groups.
171+
The parser is making use of the C<tests()>, C<results()> and C<output()> collections.
172+
173+
This parser extracts:
174+
- Test groups (`selftests: ...`)
175+
- Subtest results (`# ok ...`, `# not ok ...`)
176+
- Final group summary lines (`ok N selftests: ...`)
177+
178+
It populates internal result structures that can later be queried using standard OpenQA::Parser accessors.
179+
180+
=head1 ATTRIBUTES
181+
182+
Inherits from L<OpenQA::Parser::Format::Base>. Additional attributes include:
183+
184+
=head2 test
185+
186+
An instance of L<OpenQA::Parser::Result> representing the current test group being parsed.
187+
188+
=head2 steps
189+
190+
A temporary result object accumulating the individual subtest results for the current test group.
191+
192+
=head1 METHODS
193+
194+
=head2 _is_tap($raw)
195+
196+
Internal helper that checks whether the given content is valid TAP/KTAP and extracts version if found.
197+
Throws an exception on invalid TAP.
198+
199+
=head2 _testgroup_init($line, $test_ref, $steps_ref, $m_ref)
200+
201+
Initializes a new test group when a C<# selftests: groupname> comment is encountered.
202+
203+
=head2 _parse_subtest($result, $test_ref, $steps_ref, $m_ref)
204+
205+
Parses a subtest line of the form C<# ok 1 testname> and appends the result to the current steps.
206+
207+
=head2 _testgroup_finalize($steps_ref)
208+
209+
Finalizes the current test group and stores the accumulated results.
210+
211+
=cut
212+
213+
1;

0 commit comments

Comments
 (0)