#!/usr/bin/env python3 # Copyright Advanced Micro Devices, Inc. # SPDX-License-Identifier: MIT """ Unit tests for build_topology module. """ import os import sys import tempfile import textwrap import unittest from io import StringIO from pathlib import Path sys.path.insert(0, os.fspath(Path(__file__).parent.parent)) from configure_stage import get_stage_features from _therock_utils.build_topology import ( BuildStage, ArtifactGroup, Artifact, BuildTopology, get_topology, ) from topology_to_cmake import generate_feature_declarations class BuildTopologyTest(unittest.TestCase): """Test cases for BuildTopology class.""" def setUp(self): """Set up test fixtures.""" # Create a temporary file and close it immediately to avoid file locking on Windows with tempfile.NamedTemporaryFile( mode="w", suffix=".toml", delete=False ) as temp_file: self.topology_path = temp_file.name def tearDown(self): """Clean up test fixtures.""" if os.path.exists(self.topology_path): os.unlink(self.topology_path) def write_topology(self, content: str): """Write topology content to temp file.""" with open(self.topology_path, "w") as f: f.write(textwrap.dedent(content)) def test_empty_topology(self): """Test parsing an empty topology file.""" self.write_topology( """ [metadata] version = "1.0" """ ) topology = BuildTopology(self.topology_path) self.assertEqual(len(topology.get_build_stages()), 0) self.assertEqual(len(topology.get_artifact_groups()), 0) self.assertEqual(len(topology.get_artifacts()), 0) def test_parse_build_stages(self): """Test parsing build stages.""" self.write_topology( """ [build_stages.foundation] description = "Foundation stage" artifact_groups = ["base", "sysdeps"] [build_stages.compiler] description = "Compiler stage" artifact_groups = ["llvm"] type = "per-arch" """ ) topology = BuildTopology(self.topology_path) stages = topology.get_build_stages() self.assertEqual(len(stages), 2) foundation = topology.build_stages["foundation"] self.assertEqual(foundation.name, "foundation") self.assertEqual(foundation.description, "Foundation stage") self.assertEqual(foundation.artifact_groups, ["base", "sysdeps"]) self.assertEqual(foundation.type, "generic") compiler = topology.build_stages["compiler"] self.assertEqual(compiler.type, "per-arch") def test_parse_external_git_sources(self): """Test parsing external git sources in source sets.""" self.write_topology( """ [source_sets.optional-hrx] description = "Optional HRX" external_git_sources = [ { name = "hrx", origin = "https://github.com/ROCm/hrx.git", commit = "e642a13425f46bcf909078459dd4e07df0723a0d", path = "optional-sources/hrx" }, ] """ ) topology = BuildTopology(self.topology_path) source_set = topology.source_sets["optional-hrx"] self.assertEqual(source_set.description, "Optional HRX") self.assertEqual(len(source_set.external_git_sources), 1) hrx = source_set.external_git_sources[0] self.assertEqual(hrx.name, "hrx") self.assertEqual(hrx.origin, "https://github.com/ROCm/hrx.git") self.assertEqual(hrx.commit, "e642a13425f46bcf909078459dd4e07df0723a0d") self.assertEqual(hrx.path, "optional-sources/hrx") def test_get_source_set_for_submodule(self): """Test looking up the owning source set for a submodule.""" self.write_topology( """ [source_sets.compilers] description = "Compiler toolchain submodules" submodules = ["llvm-project", "HIPIFY", "spirv-llvm-translator"] [source_sets.rocm-libraries] description = "ROCm libraries" submodules = ["rocm-libraries"] """ ) topology = BuildTopology(self.topology_path) self.assertEqual( topology.get_source_set_for_submodule("llvm-project").name, "compilers", ) self.assertEqual( topology.get_source_set_for_submodule("rocm-libraries").name, "rocm-libraries", ) self.assertIsNone(topology.get_source_set_for_submodule("unknown-submodule")) def test_get_source_sets_for_submodules(self): """Test batch lookup of source sets from submodule names.""" self.write_topology( """ [source_sets.compilers] description = "Compiler toolchain submodules" submodules = ["llvm-project", "HIPIFY"] [source_sets.rocm-libraries] description = "ROCm libraries" submodules = ["rocm-libraries"] [source_sets.tests] description = "Tests" submodules = ["tests"] """ ) topology = BuildTopology(self.topology_path) source_sets = topology.get_source_sets_for_submodules( ["rocm-libraries", "llvm-project", "llvm-project"] ) self.assertEqual( sorted(source_set.name for source_set in source_sets), ["compilers", "rocm-libraries"], ) def test_validate_external_git_source_path(self): """Test validation rejects external sources outside optional-sources.""" self.write_topology( """ [source_sets.optional-hrx] description = "Optional HRX" external_git_sources = [ { name = "hrx", origin = "https://github.com/ROCm/hrx.git", commit = "e642a13425f46bcf909078459dd4e07df0723a0d", path = "rocm-systems/hrx" }, ] """ ) topology = BuildTopology(self.topology_path) errors = topology.validate_topology() self.assertTrue(any("must be under optional-sources" in e for e in errors)) def test_validate_conflicting_submodule_ownership(self): """Test validation rejects submodules owned by multiple source sets.""" self.write_topology( """ [source_sets.set1] description = "Set 1" submodules = ["shared-submodule"] [source_sets.set2] description = "Set 2" submodules = ["shared-submodule"] """ ) topology = BuildTopology(self.topology_path) errors = topology.validate_topology() self.assertTrue( any("Submodule 'shared-submodule' is used by both" in e for e in errors) ) def test_parse_artifact_groups(self): """Test parsing artifact groups.""" self.write_topology( """ [artifact_groups.base] description = "Base infrastructure" type = "generic" [artifact_groups.runtime] description = "Runtime components" type = "generic" artifact_group_deps = ["base"] """ ) topology = BuildTopology(self.topology_path) groups = topology.get_artifact_groups() self.assertEqual(len(groups), 2) base = topology.artifact_groups["base"] self.assertEqual(base.name, "base") self.assertEqual(base.description, "Base infrastructure") self.assertEqual(base.type, "generic") self.assertEqual(base.artifact_group_deps, []) runtime = topology.artifact_groups["runtime"] self.assertEqual(runtime.artifact_group_deps, ["base"]) def test_parse_artifacts(self): """Test parsing artifacts.""" self.write_topology( """ [artifacts.rocm-core] artifact_group = "base" type = "target-neutral" [artifacts.hip] artifact_group = "runtime" type = "target-specific" artifact_deps = ["rocm-core"] platform = "linux" """ ) topology = BuildTopology(self.topology_path) artifacts = topology.get_artifacts() self.assertEqual(len(artifacts), 2) rocm_core = topology.artifacts["rocm-core"] self.assertEqual(rocm_core.name, "rocm-core") self.assertEqual(rocm_core.artifact_group, "base") self.assertEqual(rocm_core.type, "target-neutral") self.assertEqual(rocm_core.artifact_deps, []) self.assertIsNone(rocm_core.platform) hip = topology.artifacts["hip"] self.assertEqual(hip.artifact_group, "runtime") self.assertEqual(hip.type, "target-specific") self.assertEqual(hip.artifact_deps, ["rocm-core"]) self.assertEqual(hip.platform, "linux") def test_parse_platform_disables_guarded_by_flags(self): """Test parsing platform disables guarded by build flags.""" self.write_topology( """ [artifacts.core-runtime] artifact_group = "runtime" type = "target-neutral" disable_platforms_if_flags_not_set = { windows = "HSA_WINDOWS_SHARED_RUNTIME" } """ ) topology = BuildTopology(self.topology_path) artifact = topology.artifacts["core-runtime"] self.assertEqual( artifact.disable_platforms_if_flags_not_set, {"windows": "HSA_WINDOWS_SHARED_RUNTIME"}, ) self.assertTrue(topology.is_artifact_disabled_on_platform(artifact, "windows")) self.assertFalse( topology.is_artifact_disabled_on_platform( artifact, "windows", enabled_flags={"HSA_WINDOWS_SHARED_RUNTIME"}, ) ) self.assertFalse(topology.is_artifact_disabled_on_platform(artifact, "linux")) def test_stage_features_skip_platform_disables_guarded_by_flags(self): """Test stage features skip artifacts disabled by unset flags.""" self.write_topology( """ [build_stages.runtime] description = "Runtime" artifact_groups = ["runtime"] [artifact_groups.runtime] description = "Runtime" type = "generic" [artifacts.core-runtime] artifact_group = "runtime" type = "target-neutral" feature_name = "CORE_RUNTIME" feature_group = "CORE" disable_platforms_if_flags_not_set = { windows = "HSA_WINDOWS_SHARED_RUNTIME" } """ ) topology = BuildTopology(self.topology_path) self.assertNotIn( "CORE_RUNTIME", get_stage_features(topology, "runtime", platform_name="windows"), ) self.assertIn( "CORE_RUNTIME", get_stage_features( topology, "runtime", platform_name="windows", enabled_flags={"HSA_WINDOWS_SHARED_RUNTIME"}, ), ) def test_generates_conditional_disabled_platform_feature(self): """Test generated CMake for platform disables guarded by flags.""" self.write_topology( """ [build_stages.runtime] description = "Runtime" artifact_groups = ["runtime"] [artifact_groups.runtime] description = "Runtime" type = "generic" [artifacts.core-runtime] artifact_group = "runtime" type = "target-neutral" feature_name = "CORE_RUNTIME" feature_group = "CORE" disable_platforms_if_flags_not_set = { windows = "HSA_WINDOWS_SHARED_RUNTIME" } """ ) topology = BuildTopology(self.topology_path) output = StringIO() generate_feature_declarations(topology, output) cmake = output.getvalue() self.assertIn("if(NOT THEROCK_FLAG_HSA_WINDOWS_SHARED_RUNTIME)", cmake) self.assertIn( "list(APPEND _THEROCK_CORE_RUNTIME_DISABLE_PLATFORMS windows)", cmake, ) self.assertIn("else()", cmake) self.assertIn( "DISABLE_PLATFORMS ${_THEROCK_CORE_RUNTIME_DISABLE_PLATFORMS}", cmake, ) self.assertIn( "CORE_RUNTIME can be built on ${CMAKE_SYSTEM_NAME} only with " "-DTHEROCK_FLAG_HSA_WINDOWS_SHARED_RUNTIME=ON", cmake, ) def test_get_artifacts_in_group(self): """Test getting artifacts belonging to a group.""" self.write_topology( """ [artifacts.artifact1] artifact_group = "group1" type = "target-neutral" [artifacts.artifact2] artifact_group = "group1" type = "target-neutral" [artifacts.artifact3] artifact_group = "group2" type = "target-neutral" """ ) topology = BuildTopology(self.topology_path) group1_artifacts = topology.get_artifacts_in_group("group1") self.assertEqual(len(group1_artifacts), 2) self.assertIn("artifact1", [a.name for a in group1_artifacts]) self.assertIn("artifact2", [a.name for a in group1_artifacts]) group2_artifacts = topology.get_artifacts_in_group("group2") self.assertEqual(len(group2_artifacts), 1) self.assertEqual(group2_artifacts[0].name, "artifact3") def test_get_produced_artifacts(self): """Test getting artifacts produced by a build stage.""" self.write_topology( """ [build_stages.stage1] description = "Stage 1" artifact_groups = ["group1", "group2"] [artifact_groups.group1] description = "Group 1" type = "generic" [artifact_groups.group2] description = "Group 2" type = "generic" [artifacts.artifact1] artifact_group = "group1" type = "target-neutral" [artifacts.artifact2] artifact_group = "group1" type = "target-neutral" [artifacts.artifact3] artifact_group = "group2" type = "target-neutral" [artifacts.artifact4] artifact_group = "group3" type = "target-neutral" """ ) topology = BuildTopology(self.topology_path) produced = topology.get_produced_artifacts("stage1") self.assertEqual(len(produced), 3) self.assertIn("artifact1", produced) self.assertIn("artifact2", produced) self.assertIn("artifact3", produced) self.assertNotIn("artifact4", produced) def test_get_inbound_artifacts(self): """Test getting inbound artifacts for a build stage.""" self.write_topology( """ [build_stages.stage1] description = "Stage 1" artifact_groups = ["group1"] [build_stages.stage2] description = "Stage 2" artifact_groups = ["group2"] [artifact_groups.group1] description = "Group 1" type = "generic" [artifact_groups.group2] description = "Group 2" type = "generic" artifact_group_deps = ["group1"] [artifacts.artifact1] artifact_group = "group1" type = "target-neutral" [artifacts.artifact2] artifact_group = "group2" type = "target-neutral" artifact_deps = ["artifact1"] """ ) topology = BuildTopology(self.topology_path) # Stage1 has no inbound artifacts stage1_inbound = topology.get_inbound_artifacts("stage1") self.assertEqual(len(stage1_inbound), 0) # Stage2 depends on artifacts from stage1 stage2_inbound = topology.get_inbound_artifacts("stage2") self.assertEqual(len(stage2_inbound), 1) self.assertIn("artifact1", stage2_inbound) def test_validate_missing_references(self): """Test validation catches missing references.""" self.write_topology( """ [build_stages.stage1] description = "Stage with missing group" artifact_groups = ["missing_group"] [artifact_groups.group1] description = "Group with missing dependency" type = "generic" artifact_group_deps = ["missing_dep"] [artifacts.artifact1] artifact_group = "missing_artifact_group" type = "target-neutral" artifact_deps = ["missing_artifact"] """ ) topology = BuildTopology(self.topology_path) errors = topology.validate_topology() self.assertGreater(len(errors), 0) self.assertTrue(any("missing_group" in e for e in errors)) self.assertTrue(any("missing_dep" in e for e in errors)) self.assertTrue(any("missing_artifact_group" in e for e in errors)) self.assertTrue(any("missing_artifact" in e for e in errors)) def test_validate_circular_dependencies(self): """Test validation catches circular dependencies.""" self.write_topology( """ [artifact_groups.group1] description = "Group 1" type = "generic" artifact_group_deps = ["group2"] [artifact_groups.group2] description = "Group 2" type = "generic" artifact_group_deps = ["group3"] [artifact_groups.group3] description = "Group 3" type = "generic" artifact_group_deps = ["group1"] """ ) topology = BuildTopology(self.topology_path) errors = topology.validate_topology() self.assertGreater(len(errors), 0) self.assertTrue(any("Circular dependency" in e for e in errors)) def test_get_build_order(self): """Test getting the build order based on dependencies.""" self.write_topology( """ [build_stages.foundation] description = "Foundation" artifact_groups = ["base"] [build_stages.compiler] description = "Compiler" artifact_groups = ["llvm"] [build_stages.runtime] description = "Runtime" artifact_groups = ["hip"] [artifact_groups.base] description = "Base" type = "generic" [artifact_groups.llvm] description = "LLVM" type = "generic" artifact_group_deps = ["base"] [artifact_groups.hip] description = "HIP" type = "generic" artifact_group_deps = ["llvm"] """ ) topology = BuildTopology(self.topology_path) build_order = topology.get_build_order() # Foundation should come before compiler foundation_idx = build_order.index("foundation") compiler_idx = build_order.index("compiler") self.assertLess(foundation_idx, compiler_idx) # Compiler should come before runtime runtime_idx = build_order.index("runtime") self.assertLess(compiler_idx, runtime_idx) def test_topology_reverse_indexes(self): """Test reverse indexes across source sets, groups, artifacts, and stages.""" self.write_topology( """ [source_sets.base] description = "Base" submodules = ["base"] [source_sets.runtime] description = "Runtime" submodules = ["runtime"] [source_sets.tests] description = "Tests" submodules = ["tests"] [source_sets.unused] description = "Unused" submodules = ["unused"] [build_stages.foundation] description = "Foundation" artifact_groups = ["base"] [build_stages.runtime] description = "Runtime" artifact_groups = ["hip", "rocrtst"] [artifact_groups.base] description = "Base" type = "generic" source_sets = ["base"] [artifact_groups.hip] description = "HIP" type = "generic" source_sets = ["runtime"] [artifact_groups.rocrtst] description = "Runtime tests" type = "generic" source_sets = ["runtime", "tests"] [artifact_groups.future] description = "Future group" type = "generic" source_sets = ["base"] [artifacts.base-artifact] artifact_group = "base" type = "target-neutral" [artifacts.hip-artifact] artifact_group = "hip" type = "target-neutral" [artifacts.rocrtst-artifact] artifact_group = "rocrtst" type = "target-neutral" [artifacts.future-artifact] artifact_group = "future" type = "target-neutral" """ ) topology = BuildTopology(self.topology_path) self.assertEqual( topology.get_source_set_to_artifact_groups(), { "base": ["base", "future"], "runtime": ["hip", "rocrtst"], "tests": ["rocrtst"], "unused": [], }, ) self.assertEqual( topology.get_artifact_group_to_artifacts(), { "base": ["base-artifact"], "hip": ["hip-artifact"], "rocrtst": ["rocrtst-artifact"], "future": ["future-artifact"], }, ) self.assertEqual( topology.get_artifact_group_to_build_stages(), { "base": ["foundation"], "hip": ["runtime"], "rocrtst": ["runtime"], "future": [], }, ) self.assertEqual( topology.get_artifact_to_producer_stages(), { "base-artifact": ["foundation"], "hip-artifact": ["runtime"], "rocrtst-artifact": ["runtime"], "future-artifact": [], }, ) self.assertEqual( topology.get_stage_to_source_sets(), { "foundation": ["base"], "runtime": ["runtime", "tests"], }, ) self.assertEqual( topology.get_source_set_to_stages(), { "base": ["foundation"], "runtime": ["runtime"], "tests": ["runtime"], "unused": [], }, ) self.assertEqual( topology.get_submodule_to_source_set(), { "base": "base", "runtime": "runtime", "tests": "tests", "unused": "unused", }, ) def test_stage_source_set_indexes_filter_disabled_platforms(self): """Test stage/source set indexes respect platform disabled source sets.""" self.write_topology( """ [source_sets.common] description = "Common" submodules = ["common"] [source_sets.windows-only] description = "Windows-only" submodules = ["windows-only"] disable_platforms = ["linux"] [build_stages.runtime] description = "Runtime" artifact_groups = ["runtime"] [artifact_groups.runtime] description = "Runtime" type = "generic" source_sets = ["common", "windows-only"] [artifacts.runtime-artifact] artifact_group = "runtime" type = "target-neutral" """ ) topology = BuildTopology(self.topology_path) self.assertEqual( topology.get_stage_to_source_sets(), {"runtime": ["common", "windows-only"]}, ) self.assertEqual( topology.get_stage_to_source_sets(platform="linux"), {"runtime": ["common"]}, ) self.assertEqual( topology.get_source_set_to_stages(platform="linux"), { "common": ["runtime"], "windows-only": [], }, ) def test_get_dependency_graph(self): """Test generating dependency graph.""" self.write_topology( """ [build_stages.stage1] description = "Stage 1" artifact_groups = ["group1"] [artifact_groups.group1] description = "Group 1" type = "generic" [artifacts.artifact1] artifact_group = "group1" type = "target-neutral" """ ) topology = BuildTopology(self.topology_path) graph = topology.get_dependency_graph() self.assertIn("build_stages", graph) self.assertIn("artifact_groups", graph) self.assertIn("artifacts", graph) self.assertIn("stage1", graph["build_stages"]) self.assertIn("group1", graph["artifact_groups"]) self.assertIn("artifact1", graph["artifacts"]) # Check stage details stage1_data = graph["build_stages"]["stage1"] self.assertEqual(stage1_data["type"], "generic") self.assertEqual(stage1_data["artifact_groups"], ["group1"]) self.assertIn("produced_artifacts", stage1_data) self.assertIn("artifact1", stage1_data["produced_artifacts"]) def test_invalid_stage_name(self): """Test handling of invalid stage name.""" self.write_topology( """ [build_stages.stage1] description = "Stage 1" artifact_groups = [] """ ) topology = BuildTopology(self.topology_path) with self.assertRaises(ValueError) as context: topology.get_inbound_artifacts("nonexistent_stage") self.assertIn("not found", str(context.exception)) def test_diamond_dependency_pattern(self): """Test diamond dependency pattern doesn't cause redundant processing.""" self.write_topology( """ [build_stages.stage1] description = "Stage 1" artifact_groups = ["group1"] [build_stages.stage2] description = "Stage 2" artifact_groups = ["group2"] [artifact_groups.group1] description = "Group 1" type = "generic" [artifact_groups.group2] description = "Group 2" type = "generic" artifact_group_deps = ["group1"] # Diamond pattern: # A # / \\ # B C # \\ / # D [artifacts.D] artifact_group = "group1" type = "target-neutral" [artifacts.B] artifact_group = "group1" type = "target-neutral" artifact_deps = ["D"] [artifacts.C] artifact_group = "group1" type = "target-neutral" artifact_deps = ["D"] [artifacts.A] artifact_group = "group2" type = "target-neutral" artifact_deps = ["B", "C"] """ ) topology = BuildTopology(self.topology_path) # Stage2 should get D only once, not twice stage2_inbound = topology.get_inbound_artifacts("stage2") # Count how many times D appears (should be exactly once) d_count = list(stage2_inbound).count("D") self.assertEqual(d_count, 1, "D should appear exactly once in dependencies") # Should have all three dependencies B, C, D self.assertEqual(len(stage2_inbound), 3) self.assertIn("B", stage2_inbound) self.assertIn("C", stage2_inbound) self.assertIn("D", stage2_inbound) def test_group_dependencies_include_transitive_artifact_dependencies(self): """Test group deps include the artifact deps of artifacts they pull in.""" self.write_topology( """ [build_stages.producer] description = "Producer" artifact_groups = ["base"] [build_stages.consumer] description = "Consumer" artifact_groups = ["leaf"] [artifact_groups.base] description = "Base" type = "generic" [artifact_groups.leaf] description = "Leaf" type = "generic" artifact_group_deps = ["base"] [artifacts.toolchain-runtime] artifact_group = "base" type = "target-neutral" artifact_deps = ["toolchain-frontend"] [artifacts.toolchain-frontend] artifact_group = "base" type = "target-neutral" artifact_deps = ["toolchain-base"] [artifacts.toolchain-base] artifact_group = "base" type = "target-neutral" [artifacts.leaf-artifact] artifact_group = "leaf" type = "target-neutral" """ ) topology = BuildTopology(self.topology_path) consumer_inbound = topology.get_inbound_artifacts("consumer") self.assertIn("toolchain-runtime", consumer_inbound) self.assertIn("toolchain-frontend", consumer_inbound) self.assertIn("toolchain-base", consumer_inbound) def test_complex_dependency_chain(self): """Test complex dependency chain resolution.""" self.write_topology( """ [build_stages.foundation] artifact_groups = ["base"] description = "Foundation" [build_stages.compiler] artifact_groups = ["llvm"] description = "Compiler" [build_stages.runtime] artifact_groups = ["hip"] description = "Runtime" [build_stages.libraries] artifact_groups = ["math"] description = "Libraries" [artifact_groups.base] type = "generic" description = "Base" [artifact_groups.llvm] type = "generic" artifact_group_deps = ["base"] description = "LLVM" [artifact_groups.hip] type = "generic" artifact_group_deps = ["base", "llvm"] description = "HIP" [artifact_groups.math] type = "generic" artifact_group_deps = ["hip"] description = "Math" [artifacts.base-artifact] artifact_group = "base" type = "target-neutral" [artifacts.llvm-artifact] artifact_group = "llvm" type = "target-neutral" artifact_deps = ["base-artifact"] [artifacts.hip-artifact] artifact_group = "hip" type = "target-neutral" artifact_deps = ["base-artifact", "llvm-artifact"] [artifacts.math-artifact] artifact_group = "math" type = "target-neutral" artifact_deps = ["hip-artifact"] """ ) topology = BuildTopology(self.topology_path) # Libraries stage should need all upstream artifacts libs_inbound = topology.get_inbound_artifacts("libraries") self.assertIn("hip-artifact", libs_inbound) self.assertIn("llvm-artifact", libs_inbound) self.assertIn("base-artifact", libs_inbound) # Foundation stage should need nothing foundation_inbound = topology.get_inbound_artifacts("foundation") self.assertEqual(len(foundation_inbound), 0) class RealTopologyTest(unittest.TestCase): """Assertions against the repo's actual BUILD_TOPOLOGY.toml.""" def test_hipkernelprovider_is_split_per_arch(self): # rocKE ships per-arch AOT bundles under engines/arch_content/rocke/, # so hipkernelprovider must stay target-specific and kpack-split; reverting # either drops the per-arch bundles from the device artifacts. topology = get_topology() hkp = topology.artifacts["hipkernelprovider"] self.assertEqual(hkp.type, "target-specific") self.assertIn("hipkernelprovider", hkp.split_databases) if __name__ == "__main__": unittest.main()