Skip to content

Commit fc58288

Browse files
committed
Generate api/test fuzzer tests
The prior Hub() version should get fuzz tests defined with the version bump
1 parent fea9eaf commit fc58288

File tree

1 file changed

+266
-8
lines changed

1 file changed

+266
-8
lines changed

hack/new-schema-version.py

Lines changed: 266 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,10 @@ def run(self) -> None:
527527
self.step_fix_old_ver_import_aliases,
528528
"Updating api/{old} to use versioned import aliases",
529529
),
530+
(
531+
self.step_create_conversion_tests,
532+
"Creating api/test/{old} conversion tests",
533+
),
530534
(
531535
self.step_update_template_constants,
532536
"Adding {new} template constants to pkg/providers/vsphere/constants",
@@ -1235,6 +1239,7 @@ def step_update_imports(self) -> None:
12351239
self.ctx.new_api_dir,
12361240
self.ctx.root / "vendor",
12371241
self.ctx.root / "webhooks" / "conversion" / self.ctx.old_ver,
1242+
self.ctx.root / "api" / "test" / self.ctx.old_ver,
12381243
]
12391244

12401245
for go_file in self.ctx.root.rglob("*.go"):
@@ -1396,7 +1401,260 @@ def step_fix_old_ver_import_aliases(self) -> None:
13961401
)
13971402

13981403
# -------------------------------------------------------------------------
1399-
# Step 16: Add new version template constants (pkg/providers/vsphere/constants)
1404+
# Step 16: Create api/test/OLD_VER conversion tests
1405+
# -------------------------------------------------------------------------
1406+
1407+
def step_create_conversion_tests(self) -> None:
1408+
"""Create api/test/OLD_VER with FuzzyConversion tests for all
1409+
converted types.
1410+
1411+
When a new hub is introduced, the previous hub becomes a spoke and
1412+
needs fuzz-based round-trip tests (Spoke-Hub-Spoke, Hub-Spoke-Hub)
1413+
for every root type that has conversion.
1414+
"""
1415+
old_ver = self.ctx.old_ver
1416+
new_ver = self.ctx.new_ver
1417+
prev_ver = self.ctx.prev_ver
1418+
1419+
test_dir = self.ctx.root / "api" / "test" / old_ver
1420+
if test_dir.exists():
1421+
return
1422+
1423+
src_test_dir: Path | None = None
1424+
if prev_ver:
1425+
candidate = self.ctx.root / "api" / "test" / prev_ver
1426+
if candidate.exists():
1427+
src_test_dir = candidate
1428+
1429+
self.ops.mkdir(test_dir)
1430+
1431+
root_types = _discover_root_types(self.ctx.old_api_dir)
1432+
conv_types: list[str] = []
1433+
for conv_file in self.ctx.old_api_dir.glob("*_conversion.go"):
1434+
content = self.ops.read_file(conv_file)
1435+
for m in re.finditer(r"func \(\w* ?\*(\w+)\)", content):
1436+
name = m.group(1)
1437+
if name in root_types:
1438+
conv_types.append(name)
1439+
conv_types = sorted(set(conv_types))
1440+
1441+
old_alpha_num = self._version_alpha_num(old_ver)
1442+
suite_func_name = (
1443+
f"TestV1Alpha{old_alpha_num}" if old_alpha_num
1444+
else f"Test{old_ver.title()}"
1445+
)
1446+
suite_content = self._generate_suite_test(old_ver, suite_func_name)
1447+
self.ops.write_file(
1448+
test_dir / f"{old_ver}_suite_test.go", suite_content
1449+
)
1450+
1451+
if src_test_dir and (src_test_dir / "conversion_test.go").exists():
1452+
self._copy_and_adapt_conversion_test(
1453+
src_test_dir, test_dir, conv_types
1454+
)
1455+
else:
1456+
content = self._generate_conversion_test(
1457+
old_ver, new_ver, conv_types
1458+
)
1459+
self.ops.write_file(test_dir / "conversion_test.go", content)
1460+
1461+
def _generate_suite_test(self, ver: str, func_name: str) -> str:
1462+
"""Generate the Ginkgo suite test file."""
1463+
return "\n".join([
1464+
"// © Broadcom. All Rights Reserved.",
1465+
'// The term "Broadcom" refers to Broadcom Inc. and/or its '
1466+
"subsidiaries.",
1467+
"// SPDX-License-Identifier: Apache-2.0",
1468+
"",
1469+
f"package {ver}_test",
1470+
"",
1471+
"import (",
1472+
'\t"testing"',
1473+
"",
1474+
'\t. "github.com/onsi/ginkgo/v2"',
1475+
'\t. "github.com/onsi/gomega"',
1476+
")",
1477+
"",
1478+
f"func {func_name}(t *testing.T) {{",
1479+
"\tRegisterFailHandler(Fail)",
1480+
f'\tRunSpecs(t, "{ver} Suite")',
1481+
"}",
1482+
"",
1483+
])
1484+
1485+
def _copy_and_adapt_conversion_test(
1486+
self,
1487+
src_test_dir: Path,
1488+
dst_test_dir: Path,
1489+
conv_types: list[str],
1490+
) -> None:
1491+
"""Copy conversion_test.go from the previous version's test dir and
1492+
adapt it for the new spoke version.
1493+
1494+
Preserves hand-written fuzzer override functions while adding
1495+
Context blocks for any new types.
1496+
"""
1497+
old_ver = self.ctx.old_ver
1498+
new_ver = self.ctx.new_ver
1499+
prev_ver = self.ctx.prev_ver
1500+
old_alias = self.ctx.version_alias(old_ver)
1501+
prev_alias = self.ctx.version_alias(prev_ver) if prev_ver else ""
1502+
1503+
src_file = src_test_dir / "conversion_test.go"
1504+
content = self.ops.read_file(src_file)
1505+
1506+
if prev_ver:
1507+
content = content.replace(
1508+
f"package {prev_ver}_test", f"package {old_ver}_test"
1509+
)
1510+
1511+
if prev_ver and prev_alias:
1512+
base = "github.com/vmware-tanzu/vm-operator/api"
1513+
content = content.replace(
1514+
f'{prev_alias} "{base}/{prev_ver}"',
1515+
f'{old_alias} "{base}/{old_ver}"',
1516+
)
1517+
content = content.replace(f"{prev_alias}.", f"{old_alias}.")
1518+
1519+
base = "github.com/vmware-tanzu/vm-operator/api"
1520+
for v in self.ctx.all_versions:
1521+
if v == old_ver or v == new_ver:
1522+
continue
1523+
content = content.replace(
1524+
f'vmopv1 "{base}/{v}"', f'vmopv1 "{base}/{new_ver}"'
1525+
)
1526+
content = content.replace(
1527+
f'vmopv1sysprep "{base}/{v}/sysprep"',
1528+
f'vmopv1sysprep "{base}/{new_ver}/sysprep"',
1529+
)
1530+
1531+
tested_types: set[str] = set()
1532+
for m in re.finditer(r'Context\("(\w+)"', content):
1533+
tested_types.add(m.group(1))
1534+
1535+
missing = [t for t in conv_types if t not in tested_types]
1536+
if missing:
1537+
new_blocks = self._generate_context_blocks(old_alias, missing)
1538+
content = content.replace(
1539+
"\n})\n\nvar _ = Describe(\"Client-side conversion\"",
1540+
f"{new_blocks}\n}})\n\nvar _ = Describe(\"Client-side conversion\"",
1541+
)
1542+
1543+
content = self._strip_unused_funcs(content)
1544+
1545+
self.ops.write_file(dst_test_dir / "conversion_test.go", content)
1546+
1547+
@staticmethod
1548+
def _strip_unused_funcs(content: str) -> str:
1549+
"""Remove package-level unexported functions whose names are not
1550+
referenced anywhere else in the file (i.e. defined but never called).
1551+
"""
1552+
func_pat = re.compile(
1553+
r"\n(func ([a-z]\w*)\b[^\n]*\{.*?\n\})", re.DOTALL
1554+
)
1555+
for m in reversed(list(func_pat.finditer(content))):
1556+
name = m.group(2)
1557+
rest = content[: m.start(1)] + content[m.end(1) :]
1558+
if name not in rest:
1559+
content = rest
1560+
return content
1561+
1562+
def _generate_context_blocks(
1563+
self, spoke_alias: str, types: list[str]
1564+
) -> str:
1565+
"""Generate Ginkgo Context blocks for fuzzy conversion tests."""
1566+
blocks = []
1567+
for t in types:
1568+
blocks.append(
1569+
f'\n\tContext("{t}", func() {{\n'
1570+
f"\t\tBeforeEach(func() {{\n"
1571+
f"\t\t\tinput = fuzztests.FuzzTestFuncInput{{\n"
1572+
f"\t\t\t\tScheme: scheme,\n"
1573+
f"\t\t\t\tHub: &vmopv1.{t}{{}},\n"
1574+
f"\t\t\t\tSpoke: &{spoke_alias}.{t}{{}},\n"
1575+
f"\t\t\t}}\n"
1576+
f"\t\t}})\n"
1577+
f'\t\tContext("Spoke-Hub-Spoke", func() {{\n'
1578+
f'\t\t\tIt("should get fuzzy with it", func() {{\n'
1579+
f"\t\t\t\tfuzztests.SpokeHubSpoke(input)\n"
1580+
f"\t\t\t}})\n"
1581+
f"\t\t}})\n"
1582+
f'\t\tContext("Hub-Spoke-Hub", func() {{\n'
1583+
f'\t\t\tIt("should get fuzzy with it", func() {{\n'
1584+
f"\t\t\t\tfuzztests.HubSpokeHub(input)\n"
1585+
f"\t\t\t}})\n"
1586+
f"\t\t}})\n"
1587+
f"\t}})"
1588+
)
1589+
return "\n".join(blocks)
1590+
1591+
def _generate_conversion_test(
1592+
self,
1593+
old_ver: str,
1594+
new_ver: str,
1595+
conv_types: list[str],
1596+
) -> str:
1597+
"""Generate a conversion_test.go from scratch when no previous test
1598+
directory exists to copy from."""
1599+
old_alias = self.ctx.version_alias(old_ver)
1600+
base = "github.com/vmware-tanzu/vm-operator/api"
1601+
1602+
context_blocks = self._generate_context_blocks(old_alias, conv_types)
1603+
1604+
return "\n".join([
1605+
"// © Broadcom. All Rights Reserved.",
1606+
'// The term "Broadcom" refers to Broadcom Inc. and/or its '
1607+
"subsidiaries.",
1608+
"// SPDX-License-Identifier: Apache-2.0",
1609+
"",
1610+
f"package {old_ver}_test",
1611+
"",
1612+
"import (",
1613+
'\t. "github.com/onsi/ginkgo/v2"',
1614+
'\t. "github.com/onsi/gomega"',
1615+
"",
1616+
'\t"k8s.io/apimachinery/pkg/runtime"',
1617+
"",
1618+
'\t"github.com/vmware-tanzu/vm-operator/api/test/utilconversion/fuzztests"',
1619+
f'\t{old_alias} "{base}/{old_ver}"',
1620+
f'\tvmopv1 "{base}/{new_ver}"',
1621+
")",
1622+
"",
1623+
'var _ = Describe("FuzzyConversion", Label("api", "fuzz"), func() {',
1624+
"",
1625+
"\tvar (",
1626+
"\t\tscheme *runtime.Scheme",
1627+
"\t\tinput fuzztests.FuzzTestFuncInput",
1628+
"\t)",
1629+
"",
1630+
"\tBeforeEach(func() {",
1631+
"\t\tscheme = runtime.NewScheme()",
1632+
f"\t\tExpect({old_alias}.AddToScheme(scheme)).To(Succeed())",
1633+
"\t\tExpect(vmopv1.AddToScheme(scheme)).To(Succeed())",
1634+
"\t})",
1635+
"",
1636+
"\tAfterEach(func() {",
1637+
"\t\tinput = fuzztests.FuzzTestFuncInput{}",
1638+
"\t})",
1639+
f"{context_blocks}",
1640+
"})",
1641+
"",
1642+
'var _ = Describe("Client-side conversion", func() {',
1643+
'\tIt("should convert VirtualMachine from current API version '
1644+
'to latest API version", func() {',
1645+
"\t\tscheme := runtime.NewScheme()",
1646+
f"\t\tExpect({old_alias}.AddToScheme(scheme)).To(Succeed())",
1647+
"\t\tExpect(vmopv1.AddToScheme(scheme)).To(Succeed())",
1648+
f"\t\tvm1 := &{old_alias}.VirtualMachine{{}}",
1649+
"\t\tvm2 := &vmopv1.VirtualMachine{}",
1650+
"\t\tExpect(scheme.Convert(vm1, vm2, nil)).To(Succeed())",
1651+
"\t})",
1652+
"})",
1653+
"",
1654+
])
1655+
1656+
# -------------------------------------------------------------------------
1657+
# Step 17: Add new version template constants (pkg/providers/vsphere/constants)
14001658
# -------------------------------------------------------------------------
14011659

14021660
def _version_alpha_num(self, version: str) -> str | None:
@@ -1468,7 +1726,7 @@ def step_update_template_constants(self) -> None:
14681726
self.ops.write_file(constants_go, content.replace(marker, new_block, 1))
14691727

14701728
# -------------------------------------------------------------------------
1471-
# Step 17: Demote old hub and add new hub in bootstrap_templatedata.go
1729+
# Step 18: Demote old hub and add new hub in bootstrap_templatedata.go
14721730
# -------------------------------------------------------------------------
14731731

14741732
def step_update_bootstrap_templatedata(self) -> None:
@@ -1699,7 +1957,7 @@ def step_update_bootstrap_templatedata(self) -> None:
16991957
self.ops.write_file(path, content)
17001958

17011959
# -------------------------------------------------------------------------
1702-
# Step 18: Run make generate
1960+
# Step 19: Run make generate
17031961
# -------------------------------------------------------------------------
17041962

17051963
def _run_make(self, target: str) -> None:
@@ -1751,39 +2009,39 @@ def step_run_make_generate(self) -> None:
17512009
self._run_make("generate")
17522010

17532011
# -------------------------------------------------------------------------
1754-
# Step 19: Run make generate-go-conversions
2012+
# Step 20: Run make generate-go-conversions
17552013
# -------------------------------------------------------------------------
17562014

17572015
def step_run_make_generate_go_conversions(self) -> None:
17582016
"""Run make generate-go-conversions."""
17592017
self._run_make("generate-go-conversions")
17602018

17612019
# -------------------------------------------------------------------------
1762-
# Step 20: Run make manager-only
2020+
# Step 21: Run make manager-only
17632021
# -------------------------------------------------------------------------
17642022

17652023
def step_run_make_manager_only(self) -> None:
17662024
"""Run make manager-only."""
17672025
self._run_make("manager-only")
17682026

17692027
# -------------------------------------------------------------------------
1770-
# Step 21: Run make lint-go-full
2028+
# Step 22: Run make lint-go-full
17712029
# -------------------------------------------------------------------------
17722030

17732031
def step_run_make_lint_go_full(self) -> None:
17742032
"""Run make lint-go-full."""
17752033
self._run_make("lint-go-full")
17762034

17772035
# -------------------------------------------------------------------------
1778-
# Step 22: Run make generate-api-docs
2036+
# Step 23: Run make generate-api-docs
17792037
# -------------------------------------------------------------------------
17802038

17812039
def step_run_make_generate_api_docs(self) -> None:
17822040
"""Run make generate-api-docs."""
17832041
self._run_make("generate-api-docs")
17842042

17852043
# -------------------------------------------------------------------------
1786-
# Step 23: Update documentation
2044+
# Step 24: Update documentation
17872045
# -------------------------------------------------------------------------
17882046

17892047
def step_update_docs(self) -> None:

0 commit comments

Comments
 (0)