Commit d15db7996e for bind

commit d15db7996e9c83591c9295917ed32f66238f96cc
Author: Nicki Křížek <nicki@isc.org>
Date:   Thu Sep 24 15:24:34 2026 +0000

    Replace testgen.pl with a bootstrap() in the rpzrecurse test

    The generator is plain text output with no DNS logic, so it belongs in
    the test's bootstrap() hook rather than in a Perl script run from
    setup.sh.  It lives in common.py so that the DNSRPS variant of the test
    on the maintenance branches can share it.  The output is identical to
    what the Perl script produced, except that the named configuration
    files are indented with tabs.

    Assisted-by: Claude:claude-opus-5-5

diff --git a/bin/tests/system/rpzrecurse/common.py b/bin/tests/system/rpzrecurse/common.py
new file mode 100644
index 0000000000..545b6edfd6
--- /dev/null
+++ b/bin/tests/system/rpzrecurse/common.py
@@ -0,0 +1,89 @@
+# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+#
+# SPDX-License-Identifier: MPL-2.0
+#
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, you can obtain one at https://mozilla.org/MPL/2.0/.
+#
+# See the COPYRIGHT file distributed with this work for additional
+# information regarding copyright ownership.
+
+from pathlib import Path
+from string import ascii_lowercase
+
+import isctest
+
+
+def qname(n):
+    return f"q{n:02}.l2.l1.l0"
+
+
+def write_case(
+    templates, case_id, n_queries, zones, qname_wait_recurse=False, serial=1
+):
+    """
+    Write ns2's config, query list and policy zones for one test case.
+
+    zones lists the policy zones in response-policy order, each as the
+    list of triggers it contains: a trigger type known to
+    ns2/policy.db.j2.manual, or the number of the query that a qname
+    trigger matches.
+    """
+    names = [f"{case_id}.{i:02}.policy.local" for i in range(len(zones))]
+
+    queries = [f"{qname(n)}\n" for n in range(1, n_queries + 1)]
+    Path(f"ns2/{case_id}.queries").write_text("".join(queries), encoding="utf-8")
+
+    templates.render(
+        f"ns2/named.{case_id}.conf",
+        {"policy_zones": names, "qname_wait_recurse": qname_wait_recurse},
+        template="ns2/named.case.conf.j2.manual",
+    )
+
+    for name, triggers in zip(names, zones):
+        triggers = [qname(t) if isinstance(t, int) else t for t in triggers]
+        templates.render(
+            f"ns2/db.{name}",
+            {"serial": serial, "triggers": triggers},
+            template="ns2/policy.db.j2.manual",
+        )
+
+
+def bootstrap():
+    templates = isctest.template.TemplateEngine(".")
+
+    # Group 1: one policy zone with recursion-skipping triggers only
+    write_case(templates, "1a", 1, [["client-ip"]])
+    write_case(templates, "1b", 2, [[1]])
+    write_case(templates, "1c", 1, [["client-ip", 2]])
+
+    # Group 2: 32 such zones with one qname trigger each
+    write_case(templates, "2a", 33, [[q] for q in range(1, 33)])
+
+    # Group 3: triggers that need recursion, alone and next to a qname one
+    write_case(templates, "3a", 1, [["ip"]])
+    write_case(templates, "3b", 1, [["nsdname"]])
+    write_case(templates, "3c", 1, [["nsip"]])
+    write_case(templates, "3d", 2, [["ip", 1]])
+    write_case(templates, "3e", 2, [["nsdname", 1]])
+    write_case(templates, "3f", 2, [["nsip", 1]])
+
+    # Group 4 (4aa to 4bf): 32 qname-trigger zones, with an "ip" trigger
+    # added to the zone at position n so that n+1 of the 33 queries skip
+    # recursion
+    for n in range(32):
+        case_id = f"4{ascii_lowercase[n // 26]}{ascii_lowercase[n % 26]}"
+        zones = [[q] for q in range(1, n + 1)]
+        zones += [["ip", n + 2]]
+        zones += [[q + 2] for q in range(n + 1, 32)]
+        write_case(templates, case_id, 33, zones)
+
+    # Group 5: the first zone with a recursion-requiring trigger is the pivot
+    write_case(templates, "5a", 6, [[1], [2, "ip"], [4], [5, "ip"], [6]])
+
+    # Group 6: policy update races; tests.sh copies 6b's and 6c's zone file
+    # over 6a's and reloads it, so their serials must increase
+    write_case(templates, "6a", 0, [[]], qname_wait_recurse=True)
+    write_case(templates, "6b", 0, [["nsdname"]], qname_wait_recurse=True, serial=2)
+    write_case(templates, "6c", 0, [[]], qname_wait_recurse=True, serial=3)
diff --git a/bin/tests/system/rpzrecurse/ns2/named.case.conf.j2.manual b/bin/tests/system/rpzrecurse/ns2/named.case.conf.j2.manual
new file mode 100644
index 0000000000..400beeac0f
--- /dev/null
+++ b/bin/tests/system/rpzrecurse/ns2/named.case.conf.j2.manual
@@ -0,0 +1,21 @@
+# common configuration
+include "named.conf.header";
+
+view "recursive" {
+	zone "." {
+		type hint;
+		file "root.hint";
+	};
+
+	# policy configuration to be tested
+	response-policy {
+{% for zone in policy_zones %}
+		zone "@zone@";
+{% endfor %}
+	} nsdname-enable yes nsip-enable yes{% if not qname_wait_recurse %} qname-wait-recurse no{% endif %};
+
+	# policy zones to be tested
+{% for zone in policy_zones %}
+	zone "@zone@" { type primary; file "db.@zone@"; };
+{% endfor %}
+};
diff --git a/bin/tests/system/rpzrecurse/ns2/policy.db.j2.manual b/bin/tests/system/rpzrecurse/ns2/policy.db.j2.manual
new file mode 100644
index 0000000000..f97181253d
--- /dev/null
+++ b/bin/tests/system/rpzrecurse/ns2/policy.db.j2.manual
@@ -0,0 +1,25 @@
+{#
+  Trigger records for the generated policy zones (the README describes
+  the test design).  "client-ip" and "qname" triggers let named skip
+  recursion; "ip", "nsdname" and "nsip" need the answer first.  None of
+  them ever matches: l2.l1.l0 is set up to SERVFAIL, so the rcode alone
+  tells whether recursion was attempted.  Any other trigger is the name
+  that a qname trigger matches.
+#}
+$TTL 60
+{% raw %}@{% endraw %} IN SOA root.ns ns @serial@ 3600 1800 86400 60
+     NS ns
+ns A 127.0.0.1
+{% for trigger in triggers %}
+{% if trigger == "client-ip" %}
+32.1.0.0.127.rpz-client-ip CNAME .
+{% elif trigger == "ip" %}
+32.255.255.255.255.rpz-ip CNAME .
+{% elif trigger == "nsdname" %}
+ns.example.org.rpz-nsdname CNAME .
+{% elif trigger == "nsip" %}
+32.255.255.255.255.rpz-ip CNAME .
+{% else %}
+@trigger@ CNAME .
+{% endif %}
+{% endfor %}
diff --git a/bin/tests/system/rpzrecurse/setup.sh b/bin/tests/system/rpzrecurse/setup.sh
index 6e6cca1882..5362e0d82c 100644
--- a/bin/tests/system/rpzrecurse/setup.sh
+++ b/bin/tests/system/rpzrecurse/setup.sh
@@ -15,8 +15,6 @@ set -e

 . ../conf.sh

-$PERL testgen.pl
-
 # setup policy zones for a 64-zone test
 i=1
 while test $i -le 64; do
diff --git a/bin/tests/system/rpzrecurse/testgen.pl b/bin/tests/system/rpzrecurse/testgen.pl
deleted file mode 100755
index 4131bd9976..0000000000
--- a/bin/tests/system/rpzrecurse/testgen.pl
+++ /dev/null
@@ -1,343 +0,0 @@
-#!/usr/bin/env perl
-
-# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
-#
-# SPDX-License-Identifier: MPL-2.0
-#
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0.  If a copy of the MPL was not distributed with this
-# file, you can obtain one at https://mozilla.org/MPL/2.0/.
-#
-# See the COPYRIGHT file distributed with this work for additional
-# information regarding copyright ownership.
-
-use strict;
-use warnings;
-
-my $boilerplate_header = <<'EOB';
-# common configuration
-include "named.conf.header";
-
-view "recursive" {
-    zone "." {
-        type hint;
-        file "root.hint";
-    };
-
-    # policy configuration to be tested
-    response-policy {
-EOB
-
-my $no_option = <<'EOB';
-    } nsdname-enable yes nsip-enable yes;
-
-    # policy zones to be tested
-EOB
-
-my $qname_wait_recurse = <<'EOB';
-    } nsdname-enable yes nsip-enable yes qname-wait-recurse no;
-
-    # policy zones to be tested
-EOB
-
-my $boilerplate_end = <<'EOB';
-};
-EOB
-
-my $policy_option = $qname_wait_recurse;
-
-my $serialnum = "1";
-my $policy_zone_header = <<'EOH';
-$TTL 60
-@ IN SOA root.ns ns SERIAL 3600 1800 86400 60
-     NS ns
-ns A 127.0.0.1
-EOH
-
-sub policy_client_ip {
-    return "32.1.0.0.127.rpz-client-ip CNAME .\n";
-}
-
-sub policy_qname {
-    my $query_nbr = shift;
-    return sprintf "q%02d.l2.l1.l0 CNAME .\n", $query_nbr;
-}
-
-sub policy_ip {
-    return "32.255.255.255.255.rpz-ip CNAME .\n";
-}
-
-sub policy_nsdname {
-    return "ns.example.org.rpz-nsdname CNAME .\n";
-}
-
-sub policy_nsip {
-    return "32.255.255.255.255.rpz-ip CNAME .\n";
-}
-
-my %static_triggers = (
-    'client-ip' => \&policy_client_ip,
-    'ip'        => \&policy_ip,
-    'nsdname'   => \&policy_nsdname,
-    'nsip'      => \&policy_nsip,
-);
-
-sub mkconf {
-    my $case_id = shift;
-    my $n_queries = shift;
-
-    { # generate the query list
-        my $query_list_filename = "ns2/$case_id.queries";
-        my $query_list_fh;
-
-        open $query_list_fh, ">$query_list_filename" or die;
-
-        for( my $i = 1; $i <= $n_queries; $i++ ) {
-            print $query_list_fh sprintf "q%02d.l2.l1.l0\n", $i;
-        }
-    }
-
-    my @zones;
-
-    { # generate the conf file
-        my $conf_filename = "ns2/named.$case_id.conf";
-
-        my $conf_fh;
-
-        open $conf_fh, ">$conf_filename" or die;
-
-        print $conf_fh $boilerplate_header;
-
-        my $zone_seq = 0;
-
-        @zones = map {
-            [
-                sprintf( "$case_id.%02d.policy.local", $zone_seq++ ),
-                $_,
-            ];
-        } @_;
-
-        print $conf_fh map { qq{        zone "$_->[0]";\n} } @zones;
-
-        print $conf_fh $policy_option;
-
-        print $conf_fh map { qq{    zone "$_->[0]" { type primary; file "db.$_->[0]"; };\n} } @zones;
-
-        print $conf_fh $boilerplate_end;
-    }
-
-    # generate the policy zone contents
-    foreach my $policy_zone_info( @zones ) {
-        my $policy_zone_name = $policy_zone_info->[0];
-        my $policy_zone_contents = $policy_zone_info->[1];
-
-        my $policy_zone_filename = "ns2/db.$policy_zone_name";
-        my $policy_zone_fh;
-
-        open $policy_zone_fh, ">$policy_zone_filename" or die;
-
-        my $header = $policy_zone_header;
-        $header =~ s/SERIAL/$serialnum/;
-        print $policy_zone_fh $header;
-
-        foreach my $trigger( @$policy_zone_contents ) {
-            if( exists $static_triggers{$trigger} ) {
-                # matches a trigger type with a static value
-                print $policy_zone_fh $static_triggers{$trigger}->();
-            }
-            else {
-                # a qname trigger, where what was specified is the query number it should match
-                print $policy_zone_fh policy_qname( $trigger );
-            }
-        }
-    }
-}
-
-mkconf(
-    '1a',
-    1,
-    [ 'client-ip' ],
-);
-
-mkconf(
-    '1b',
-    2,
-    [ 1 ],
-);
-
-mkconf(
-    '1c',
-    1,
-    [ 'client-ip', 2 ],
-);
-
-mkconf(
-    '2a',
-    33,
-    map { [ $_ ]; }  1 .. 32
-);
-
-mkconf(
-    '3a',
-    1,
-    [ 'ip' ],
-);
-
-mkconf(
-    '3b',
-    1,
-    [ 'nsdname' ],
-);
-
-mkconf(
-    '3c',
-    1,
-    [ 'nsip' ],
-);
-
-mkconf(
-    '3d',
-    2,
-    [ 'ip', 1 ]
-);
-
-mkconf(
-    '3e',
-    2,
-    [ 'nsdname', 1 ]
-);
-
-mkconf(
-    '3f',
-    2,
-    [ 'nsip', 1 ]
-);
-
-{
-    my $seq_code = 'aa';
-    my $seq_nbr = 0;
-
-    while( $seq_nbr < 32 ) {
-
-        mkconf(
-            "4$seq_code",
-            33,
-            ( map { [ $_ ]; } 1 .. $seq_nbr ),
-            [ 'ip', $seq_nbr + 2 ],
-            ( map { [ $_ + 2 ]; } ($seq_nbr + 1) .. 31 ),
-        );
-
-        $seq_code++;
-        $seq_nbr++;
-    }
-}
-
-mkconf(
-    '5a',
-    6,
-    [ 1 ],
-    [ 2, 'ip' ],
-    [ 4 ],
-    [ 5, 'ip' ],
-    [ 6 ],
-);
-
-$policy_option = $no_option;
-
-mkconf(
-    '6a',
-    0,
-    [ ],
-);
-
-$serialnum = "2";
-mkconf(
-    '6b',
-    0,
-    [ 'nsdname' ],
-);
-
-$serialnum = "3";
-mkconf(
-    '6c',
-    0,
-    [ ],
-);
-
-__END__
-
-0x01 - has client-ip
-    32.1.0.0.127.rpz-client-ip CNAME .
-0x02 - has qname
-    qX.l2.l1.l0 CNAME .
-0x10 - has ip
-    32.255.255.255.255.rpz-ip CNAME .
-0x20 - has nsdname
-    ns.example.org.rpz-nsdname CNAME .
-0x40 - has nsip
-    32.255.255.255.255.rpz-nsip CNAME .
-
-$case.$seq.policy.local
-
-case 1a = 0x01
-    .q01 = (00,0x01)=-r
-case 1b = 0x02
-    .q01 = (00,0x02)=-r
-    .q02 = (--,----)=+r
-case 1c = 0x03
-    .q01 = (00,0x01)=-r
-
-case 2a = 0x03{32}
-    .q01 = (00,0x02)=-r
-    .q02 = (01,0x02)=-r
-     ...
-    .q31 = (30,0x02)=-r
-    .q32 = (31,0x02)=-r
-    .q33 = (--,----)=+r
-
-case 3a = 0x10
-    .q01 = (00,0x10)=+r
-case 3b = 0x20
-    .q01 = (00,0x20)=+r
-case 3c = 0x40
-    .q01 = (00,0x40)=+r
-case 3d = 0x12
-    .q01 = (00,0x10)=+r
-    .q02 = (00,0x02)=-r
-case 3e = 0x22
-    .q01 = (00,0x20)=+r
-    .q02 = (00,0x02)=-r
-case 3f = 0x42
-    .q01 = (00,0x40)=+r
-    .q02 = (00,0x02)=-r
-
-case 4aa = 0x12,0x02{31}
-    .q01 = (00,0x10)=+r
-    .q02 = (00,0x02)=-r
-    .q03 = (01,0x02)=+r
-     ...
-    .q32 = (30,0x02)=+r
-    .q33 = (31,0x02)=+r
-case 4__ = 0x02{n(1->30)},0x12,0x02{31-n}
-    .q01 = (00,0x02)=-r
-     ...
-    .q(n+1) = (n,0x10)=+r
-    .q(n+2) = (n,0x02)=-r
-     ...
-    .q33 = (31,0x02)=+r
-case 4bf = 0x02{31},0x12
-    .q01 = (00,0x02)=-r
-    .q02 = (01,0x02)=-r
-     ...
-    .q31 = (30,0x02)=-r
-    .q32 = (31,0x10)=+r
-    .q33 = (31,0x02)=-r
-
-case 5a = 0x02,0x12,0x02,0x12,0x02
-    .q01 = (00,0x02)=-r
-    .q02 = (01,0x02)=-r
-    .q03 = (01,0x10)=+r
-    .q04 = (02,0x02)=+r
-    .q05 = (03,0x02)=+r
-    .q06 = (04,0x02)=+r
-
diff --git a/bin/tests/system/rpzrecurse/tests_sh_rpzrecurse.py b/bin/tests/system/rpzrecurse/tests_sh_rpzrecurse.py
index d5de64241f..d0941ef876 100644
--- a/bin/tests/system/rpzrecurse/tests_sh_rpzrecurse.py
+++ b/bin/tests/system/rpzrecurse/tests_sh_rpzrecurse.py
@@ -11,6 +11,8 @@

 import pytest

+from rpzrecurse import common
+
 pytestmark = pytest.mark.extra_artifacts(
     [
         "dig.out.*",
@@ -24,5 +26,9 @@ pytestmark = pytest.mark.extra_artifacts(
 )


+def bootstrap():
+    common.bootstrap()
+
+
 def test_rpzrecurse(run_tests_sh):
     run_tests_sh()