@@ -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+ "\t RegisterFailHandler(Fail)" ,
1480+ f'\t RunSpecs(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 \n var _ = Describe(\" Client-side conversion\" " ,
1540+ f"{ new_blocks } \n }})\n \n var _ = 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 \t Context("{ t } ", func() {{\n '
1570+ f"\t \t BeforeEach(func() {{\n "
1571+ f"\t \t \t input = fuzztests.FuzzTestFuncInput{{\n "
1572+ f"\t \t \t \t Scheme: scheme,\n "
1573+ f"\t \t \t \t Hub: &vmopv1.{ t } {{}},\n "
1574+ f"\t \t \t \t Spoke: &{ spoke_alias } .{ t } {{}},\n "
1575+ f"\t \t \t }}\n "
1576+ f"\t \t }})\n "
1577+ f'\t \t Context("Spoke-Hub-Spoke", func() {{\n '
1578+ f'\t \t \t It("should get fuzzy with it", func() {{\n '
1579+ f"\t \t \t \t fuzztests.SpokeHubSpoke(input)\n "
1580+ f"\t \t \t }})\n "
1581+ f"\t \t }})\n "
1582+ f'\t \t Context("Hub-Spoke-Hub", func() {{\n '
1583+ f'\t \t \t It("should get fuzzy with it", func() {{\n '
1584+ f"\t \t \t \t fuzztests.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'\t vmopv1 "{ base } /{ new_ver } "' ,
1621+ ")" ,
1622+ "" ,
1623+ 'var _ = Describe("FuzzyConversion", Label("api", "fuzz"), func() {' ,
1624+ "" ,
1625+ "\t var (" ,
1626+ "\t \t scheme *runtime.Scheme" ,
1627+ "\t \t input fuzztests.FuzzTestFuncInput" ,
1628+ "\t )" ,
1629+ "" ,
1630+ "\t BeforeEach(func() {" ,
1631+ "\t \t scheme = runtime.NewScheme()" ,
1632+ f"\t \t Expect({ old_alias } .AddToScheme(scheme)).To(Succeed())" ,
1633+ "\t \t Expect(vmopv1.AddToScheme(scheme)).To(Succeed())" ,
1634+ "\t })" ,
1635+ "" ,
1636+ "\t AfterEach(func() {" ,
1637+ "\t \t input = fuzztests.FuzzTestFuncInput{}" ,
1638+ "\t })" ,
1639+ f"{ context_blocks } " ,
1640+ "})" ,
1641+ "" ,
1642+ 'var _ = Describe("Client-side conversion", func() {' ,
1643+ '\t It("should convert VirtualMachine from current API version '
1644+ 'to latest API version", func() {' ,
1645+ "\t \t scheme := runtime.NewScheme()" ,
1646+ f"\t \t Expect({ old_alias } .AddToScheme(scheme)).To(Succeed())" ,
1647+ "\t \t Expect(vmopv1.AddToScheme(scheme)).To(Succeed())" ,
1648+ f"\t \t vm1 := &{ old_alias } .VirtualMachine{{}}" ,
1649+ "\t \t vm2 := &vmopv1.VirtualMachine{}" ,
1650+ "\t \t Expect(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