# noqa: PLC0302
import itertools
import math
import os
import re
import tempfile
from typing import List, Tuple

import omni.graph.core as og
import omni.graph.core._unstable as ogu
import omni.graph.core.tests as ogts
import omni.graph.tools.ogn as ogn
import omni.kit.test
import omni.usd
from pxr import OmniGraphSchema, Sdf

DESCRIPTION_METADATA_KEY = f"omni:graph:{ogn.MetadataKeys.DESCRIPTION}"
DISPLAYNAME_METADATA_KEY = f"omni:graph:{ogn.MetadataKeys.UI_NAME}"

# ------------------------------------------------------------------------


def get_node(graph: og.Graph, node_name: str) -> og.Node:
    return graph.get_node(graph.get_path_to_graph() + "/" + node_name)


# ------------------------------------------------------------------------


def get_node_type_from_schema_prim(prim: OmniGraphSchema.OmniGraphCompoundNodeType) -> str:
    """Returns the node type from an omni graph prim"""
    return f"{prim.GetOmniGraphNamespaceAttr().Get()}.{prim.GetPrim().GetPath().name}"


# ------------------------------------------------------------------------


class TestCompoundNodes(ogts.OmniGraphTestCase):

    _simple_test_file = "TestCompoundGraph.usda"
    _test_file_with_reference = "TestCompoundWithExternalReference.usda"
    _test_file_with_default_values = "TestCompoundDefaultValues.usda"
    _test_preschema_file = "TestPreSchemaCompoundNode.usda"
    _test_graph_path = "/World/TestGraph"
    _node_library_test_file = os.path.join(os.path.dirname(__file__), "data", "TestNodeLibrary.usda")
    _library_compound_paths = [
        "/World/Compounds/Madd",
        "/World/Compounds/DebugString",
        "/World/Compounds/Deg2Rad",
        "/World/Compounds/CelciusToFahrenheit",
    ]

    # -------------------------------------------------------------------------

    def get_compound_folder(self):
        return ogu.get_default_compound_node_type_folder()

    def get_compound_path(self):
        return f"{self.get_compound_folder()}/Compound"

    def get_compound_graph_path(self):
        return f"{self.get_compound_path()}/Graph"

    # -------------------------------------------------------------------------

    async def tearDown(self):
        omni.timeline.get_timeline_interface().stop()
        await super().tearDown()

    # -------------------------------------------------------------------------

    async def test_load_file_with_compound_node_def(self):
        """
        Tests that a usd file containing a compound node definition loads the compound node and adds
        it to the node database
        """
        (result, error) = await ogts.load_test_file(self._simple_test_file, use_caller_subdirectory=True)
        self.assertTrue(result, error)

        # load the node type by path
        node_type = og.get_node_type(self.get_compound_path())
        self.assertIsNotNone(node_type)
        expected_node_type = "local.nodes." + self.get_compound_path().rsplit("/", maxsplit=1)[-1]
        self.assertEquals(node_type.get_node_type(), expected_node_type)
        metadata = node_type.get_all_metadata()

        # verify it has expected metadata
        self.assertEquals(metadata["__categories"], "Compounds")
        self.assertEquals(metadata["uiName"], "Compound")
        self.assertEquals(metadata["tags"], "Compounds")

    # -------------------------------------------------------------------------
    async def test_usd_changes_reflect_in_node_type(self):
        """
        Tests that changing OmniGraphCompoundNodeType values are reflected in the compound node

        """
        stage = omni.usd.get_context().get_stage()

        ogu.cmds.CreateCompoundNodeType(compound_name="Compound", graph_name="Graph")
        self.assertTrue(stage.GetPrimAtPath(self.get_compound_path()).IsValid())

        # Change the schema_prim values
        schema_prim = OmniGraphSchema.OmniGraphCompoundNodeType(stage.GetPrimAtPath(self.get_compound_path()))
        schema_prim.GetOmniGraphCategoriesAttr().Set(["CatA", "CatB"])
        schema_prim.GetOmniGraphDescriptionAttr().Set("Changed description")
        schema_prim.GetOmniGraphTagsAttr().Set(["TagA", "TagB"])
        schema_prim.GetOmniGraphUiNameAttr().Set("FakeUIName")

        # validate they are changed in the node type
        node_type = og.get_node_type(self.get_compound_path())
        metadata = node_type.get_all_metadata()
        self.assertEquals(metadata["__categories"], "CatA,CatB")
        self.assertEquals(metadata["tags"], "CatA,CatB")
        self.assertEquals(metadata["__description"], "Changed description")
        self.assertEquals(metadata["uiName"], "FakeUIName")

    # -------------------------------------------------------------------------
    async def test_create_node_types_save_and_load(self):
        """Tests that a created node types save to USD and reload correctly"""

        stage = omni.usd.get_context().get_stage()

        # create a compound node
        ogu.cmds.CreateCompoundNodeType(compound_name="Compound", graph_name="Graph")
        self.assertTrue(stage.GetPrimAtPath(self.get_compound_path()).IsValid())

        with tempfile.TemporaryDirectory() as tmpdirname:
            # save the file
            tmp_file_path = os.path.join(tmpdirname, "tmp.usda")
            result = omni.usd.get_context().save_as_stage(tmp_file_path)
            self.assertTrue(result)

            # refresh the stage
            await omni.usd.get_context().new_stage_async()

            # validate the node type was unloaded with the stage
            self.assertFalse(og.get_node_type(self.get_compound_path()).is_valid())

            # reload the file back
            (result, error) = await ogts.load_test_file(tmp_file_path)
            self.assertTrue(result, error)

        # load the node type by path
        node_type = og.get_node_type(self.get_compound_path())
        self.assertIsNotNone(node_type)

        expected_node_type = "local.nodes." + self.get_compound_path().rsplit("/", maxsplit=1)[-1]
        self.assertEquals(node_type.get_node_type(), expected_node_type)

    # -------------------------------------------------------------------------
    def _create_and_test_graph_from_add_compound(
        self, compound_path, input_a="input_a", input_b="input_b", output="value", should_add: bool = True
    ) -> og.Graph:
        """Create and test a graph around a simple compound of an add node"""

        # create a graph that uses the compound graph
        keys = og.Controller.Keys
        controller = og.Controller()
        (graph, nodes, _, _) = controller.edit(
            self._test_graph_path,
            {
                keys.CREATE_NODES: [
                    ("Compound", compound_path),
                    ("Constant_a", "omni.graph.nodes.ConstantDouble"),
                    ("Constant_b", "omni.graph.nodes.ConstantDouble"),
                ],
                keys.CONNECT: [
                    ("Constant_a.inputs:value", f"Compound.inputs:{input_a}"),
                    ("Constant_b.inputs:value", f"Compound.inputs:{input_b}"),
                ],
                keys.SET_VALUES: [("Constant_a.inputs:value", 1.0), ("Constant_b.inputs:value", 2.0)],
            },
        )

        self.assertTrue(nodes[0].get_attribute_exists(f"inputs:{input_a}"))
        self.assertTrue(nodes[0].get_attribute_exists(f"inputs:{input_b}"))
        self.assertTrue(nodes[0].get_attribute_exists(f"outputs:{output}"))

        graph.evaluate()

        result = nodes[0].get_attribute(f"outputs:{output}").get()
        if should_add:
            self.assertEquals(result, 3.0)

        return graph

    # ------------------------------------------------------------------------
    def _create_graph_with_compound_node(self, compound_path: str) -> Tuple[og.Graph, og.Node]:
        """Create a graph that simply uses the compound node type specified."""

        keys = og.Controller.Keys
        controller = og.Controller()
        (graph, nodes, _, _) = controller.edit(
            self._test_graph_path, {keys.CREATE_NODES: [("Compound", compound_path)]}
        )
        return (graph, nodes[0])

    # ------------------------------------------------------------------------

    def _create_test_compound(
        self, compound_name, graph_name="Graph", namespace="local.nodes.", compound_dir=None, evaluator_type=None
    ):
        """Creates a compound node/graph that wraps an add node"""
        # create a compound node
        (success, schema_prim) = ogu.cmds.CreateCompoundNodeType(
            compound_name=compound_name,
            graph_name=graph_name,
            namespace=namespace,
            folder=compound_dir,
            evaluator_type=evaluator_type,
        )
        if not success:
            raise og.OmniGraphError("CreateNodeType failed")

        compound_path = str(schema_prim.GetPrim().GetPath())
        graph_path = f"{compound_path}/{graph_name}"
        node_path = f"{graph_path}/Add_Node"
        node_type = get_node_type_from_schema_prim(schema_prim)

        # Create a node in the sub graph
        og.cmds.CreateNode(
            graph=og.get_graph_by_path(graph_path),
            node_path=node_path,
            node_type="omni.graph.nodes.Add",
            create_usd=True,
        )

        # assign inputs and outputs
        ogu.cmds.CreateCompoundNodeTypeInput(
            node_type=node_type, input_name="input_a", attribute_path=Sdf.Path(f"{node_path}.inputs:a")
        )
        ogu.cmds.CreateCompoundNodeTypeInput(
            node_type=node_type, input_name="input_b", attribute_path=Sdf.Path(f"{node_path}.inputs:b")
        )
        ogu.cmds.CreateCompoundNodeTypeOutput(
            node_type=node_type, output_name="value", attribute_path=Sdf.Path(f"{node_path}.outputs:sum")
        )

    # -------------------------------------------------------------------------

    async def test_create_and_use_compound_node(self):
        """Tests manual creation, assignment and evaluation of a compound node"""

        self._create_test_compound("Compound")
        self._create_and_test_graph_from_add_compound(f"{self.get_compound_folder()}/Compound")

    # -------------------------------------------------------------------------

    async def test_create_and_use_compound_node_with_qualifiedname(self):
        """Tests that graphs can be referenced by the qualified name"""

        self._create_test_compound("Compound")
        self._create_and_test_graph_from_add_compound("local.nodes.Compound")

    # -------------------------------------------------------------------------

    async def test_create_and_use_compound_node_with_namespace(self):
        """Tests that graphs can be referenced using a different namespace"""

        self._create_test_compound("Compound", "Graph", "test.nodes.")
        self._create_and_test_graph_from_add_compound("test.nodes.Compound")

    # -------------------------------------------------------------------------

    async def test_create_and_rename_compound_node(self):
        """Tests created graphs can handle"""
        self._create_test_compound("Compound", "Graph")
        graph = self._create_and_test_graph_from_add_compound("local.nodes.Compound")
        stage = omni.usd.get_context().get_stage()

        # rename the compound node
        new_compound_path = f"{self.get_compound_folder()}/NewCompound"
        omni.kit.commands.execute("MovePrim", path_from=self.get_compound_path(), path_to=new_compound_path)
        await omni.kit.app.get_app().next_update_async()

        # validate the graph still updates
        node = graph.get_node(graph.get_path_to_graph() + "/Constant_a")
        node.get_attribute("inputs:value").set(2.0)
        graph.evaluate()
        node = graph.get_node(graph.get_path_to_graph() + "/Compound")
        self.assertEquals(node.get_attribute("outputs:value").get(), 4.0)

        # validate the node type is properly updated
        node = OmniGraphSchema.OmniGraphNode(stage.GetPrimAtPath(node.get_prim_path()))
        self.assertEquals("local.nodes.NewCompound", node.GetNodeTypeAttr().Get())

    # -------------------------------------------------------------------------

    async def test_change_compound_namespace(self):
        """Tests that changing the compound namespace is reflected in the nodes"""
        self._create_test_compound("Compound")
        graph = self._create_and_test_graph_from_add_compound(self.get_compound_path())

        stage = omni.usd.get_context().get_stage()

        # change the node type namespace
        schema_prim = OmniGraphSchema.OmniGraphCompoundNodeType(stage.GetPrimAtPath(self.get_compound_path()))
        schema_prim.GetOmniGraphNamespaceAttr().Set("test.nodes")

        # validate the node type itself updates
        node = OmniGraphSchema.OmniGraphNode(stage.GetPrimAtPath(f"{self._test_graph_path}/Compound"))
        self.assertEquals("test.nodes.Compound", node.GetNodeTypeAttr().Get())

        # validate the graph still updates
        node = graph.get_node(graph.get_path_to_graph() + "/Constant_a")
        node.get_attribute("inputs:value").set(2.0)
        graph.evaluate()
        node = graph.get_node(graph.get_path_to_graph() + "/Compound")
        self.assertEquals(node.get_attribute("outputs:value").get(), 4.0)

    # -------------------------------------------------------------------------

    async def test_node_type_command_undo_redo(self):
        """Tests node type commands support undo/redo"""
        stage = omni.usd.get_context().get_stage()

        input_attribute_path = f"{self.get_compound_path()}.omni:graph:input:"
        output_attribute_path = f"{self.get_compound_path()}.omni:graph:output:"

        # create a compound node
        ogu.cmds.CreateCompoundNodeType(compound_name="Compound", graph_name="Graph")
        self.assertTrue(stage.GetPrimAtPath(self.get_compound_path()).IsValid())
        self.assertTrue(stage.GetPrimAtPath(self.get_compound_graph_path()).IsValid())
        omni.kit.undo.undo()
        self.assertFalse(stage.GetPrimAtPath(self.get_compound_path()).IsValid())
        self.assertFalse(stage.GetPrimAtPath(self.get_compound_graph_path()).IsValid())
        omni.kit.undo.redo()
        self.assertTrue(stage.GetPrimAtPath(self.get_compound_path()).IsValid())
        self.assertTrue(stage.GetPrimAtPath(self.get_compound_graph_path()).IsValid())

        # Create a node in the sub graph
        og.cmds.CreateNode(
            graph=og.get_graph_by_path(self.get_compound_graph_path()),
            node_path=self.get_compound_graph_path() + "/Add_Node",
            node_type="omni.graph.nodes.Add",
            create_usd=True,
        )

        ogu.cmds.CreateCompoundNodeTypeInput(
            node_type=self.get_compound_path(),
            input_name="input_a",
            attribute_path=Sdf.Path(self.get_compound_path() + "/Graph/Add_Node.inputs:a"),
        )
        self.assertTrue(stage.GetRelationshipAtPath(f"{input_attribute_path}input_a").IsValid())
        omni.kit.undo.undo()
        self.assertFalse(stage.GetRelationshipAtPath(f"{input_attribute_path}input_a").IsValid())
        omni.kit.undo.redo()
        self.assertTrue(stage.GetRelationshipAtPath(f"{input_attribute_path}input_a").IsValid())

        ogu.cmds.CreateCompoundNodeTypeOutput(
            node_type=self.get_compound_path(),
            output_name="value",
            attribute_path=Sdf.Path(self.get_compound_graph_path() + "/Add_Node.outputs:sum"),
        )
        self.assertTrue(stage.GetRelationshipAtPath(f"{output_attribute_path}value").IsValid())
        omni.kit.undo.undo()
        self.assertFalse(stage.GetRelationshipAtPath(f"{output_attribute_path}value").IsValid())
        omni.kit.undo.redo()
        self.assertTrue(stage.GetRelationshipAtPath(f"{output_attribute_path}value").IsValid())

        ogu.cmds.RemoveCompoundNodeTypeInput(
            node_type=self.get_compound_path(),
            input_name="input_a",
        )
        self.assertFalse(stage.GetRelationshipAtPath(f"{input_attribute_path}input_a").IsValid())
        omni.kit.undo.undo()
        self.assertTrue(stage.GetRelationshipAtPath(f"{input_attribute_path}input_a").IsValid())

        ogu.cmds.RemoveCompoundNodeTypeOutput(
            node_type=self.get_compound_path(),
            output_name="value",
        )
        self.assertFalse(stage.GetRelationshipAtPath(f"{output_attribute_path}value").IsValid())
        omni.kit.undo.undo()
        self.assertTrue(stage.GetRelationshipAtPath(f"{output_attribute_path}value").IsValid())

        # set up the second input and test to make sure the graph works
        ogu.cmds.CreateCompoundNodeTypeInput(
            node_type=self.get_compound_path(),
            input_name="input_b",
            attribute_path=Sdf.Path(self.get_compound_graph_path() + "/Add_Node.inputs:b"),
        )
        self._create_and_test_graph_from_add_compound(self.get_compound_path())

    # -------------------------------------------------------------------------
    async def test_nested_compound_nodes(self):
        """Tests that nested compounds can be created and execute as expected"""
        # create an add compound for the inner node
        self._create_test_compound("innerCompound")
        inner_compound_path = f"{self.get_compound_folder()}/innerCompound"

        ogu.cmds.CreateCompoundNodeType(compound_name="outerCompound", graph_name="Graph")
        outer_compound_path = f"{self.get_compound_folder()}/outerCompound"
        outer_graph_path = f"{outer_compound_path}/Graph"
        outer_graph_node_path = f"{outer_graph_path}/Node"

        # Create a node in the sub graph that uses the inner compound
        og.cmds.CreateNode(
            graph=og.get_graph_by_path(outer_graph_path),
            node_path=outer_graph_node_path,
            node_type=inner_compound_path,
            create_usd=True,
        )

        ogu.cmds.CreateCompoundNodeTypeInput(
            node_type=outer_compound_path,
            input_name="a",
            attribute_path=Sdf.Path(f"{outer_graph_node_path}.inputs:input_a"),
        )
        ogu.cmds.CreateCompoundNodeTypeInput(
            node_type=outer_compound_path,
            input_name="b",
            attribute_path=Sdf.Path(f"{outer_graph_node_path}.inputs:input_b"),
        )
        ogu.cmds.CreateCompoundNodeTypeOutput(
            node_type=outer_compound_path,
            output_name="result",
            attribute_path=Sdf.Path(f"{outer_graph_node_path}.outputs:value"),
        )

        self._create_and_test_graph_from_add_compound(outer_compound_path, "a", "b", "result")

    # -------------------------------------------------------------------------
    async def test_multiple_compounds_used(self):
        """Tests that multiple compounds of the same type can be used in a single graphs"""

        self._create_test_compound("Compound")

        # create a graph that uses the compound graph
        #            ------------            -------------
        # [Const a]->|Compound a| ------->   |Compound b |
        # [Const b]  |          | [Const c]->|           |
        #            ------------            -------------

        keys = og.Controller.Keys
        controller = og.Controller()
        (graph, nodes, _, _) = controller.edit(
            self._test_graph_path,
            {
                keys.CREATE_NODES: [
                    ("Compound_a", self.get_compound_path()),
                    ("Compound_b", self.get_compound_path()),
                    ("Constant_a", "omni.graph.nodes.ConstantDouble"),
                    ("Constant_b", "omni.graph.nodes.ConstantDouble"),
                    ("Constant_c", "omni.graph.nodes.ConstantDouble"),
                ],
                keys.CONNECT: [
                    ("Constant_a.inputs:value", "Compound_a.inputs:input_a"),
                    ("Constant_b.inputs:value", "Compound_a.inputs:input_b"),
                    ("Compound_a.outputs:value", "Compound_b.inputs:input_a"),
                    ("Constant_c.inputs:value", "Compound_b.inputs:input_b"),
                ],
                keys.SET_VALUES: [
                    ("Constant_a.inputs:value", 1.0),
                    ("Constant_b.inputs:value", 2.0),
                    ("Constant_c.inputs:value", 3.0),
                ],
            },
        )

        self.assertTrue(nodes[0].get_attribute_exists("inputs:input_a"))
        self.assertTrue(nodes[0].get_attribute_exists("inputs:input_b"))
        self.assertTrue(nodes[0].get_attribute_exists("outputs:value"))
        self.assertTrue(nodes[1].get_attribute_exists("inputs:input_a"))
        self.assertTrue(nodes[1].get_attribute_exists("inputs:input_b"))
        self.assertTrue(nodes[1].get_attribute_exists("outputs:value"))

        graph.evaluate()

        # check intermediate result
        result = nodes[0].get_attribute("outputs:value").get()
        self.assertEquals(result, 3.0)

        # check final result
        result = nodes[1].get_attribute("outputs:value").get()
        self.assertEquals(result, 6.0)

    # -------------------------------------------------------------------------
    async def test_compound_graph_does_not_run(self):
        """
        Tests that when creating a compound graph, the compound graph itself
        does not evaluate
        """

        compound_name = "Compound"
        graph_name = "Graph"

        # create a compound node
        ogu.cmds.CreateCompoundNodeType(compound_name=compound_name, graph_name=graph_name)

        compound_path = f"{self.get_compound_folder()}/{compound_name}"
        graph_path = f"{compound_path}/{graph_name}"

        keys = og.Controller.Keys
        controller = og.Controller()
        (_, (time_node, to_string_node), _, _) = controller.edit(
            graph_path,
            {
                keys.CREATE_NODES: [
                    ("Time", "omni.graph.nodes.ReadTime"),
                    ("ToString", "omni.graph.nodes.ToString"),
                ],
                keys.CONNECT: [("Time.outputs:timeSinceStart", "ToString.inputs:value")],
            },
        )

        for _ in range(1, 5):
            await omni.kit.app.get_app().next_update_async()
            self.assertEquals(time_node.get_compute_count(), 0)
            self.assertEquals(to_string_node.get_compute_count(), 0)

    # -------------------------------------------------------------------------
    async def test_replace_with_compound_command(self):
        """Tests the replace with compound command under different scenarios"""
        graph_path = "/World/Graph"

        def build_graph(graph_path: str) -> Tuple[og.Node]:

            # [ConstA] -----> [Add]---v
            # [ConstB] -------^       [Add]---v
            # [ConstC] ---------------^       [Add]--->[Magnitude]
            # [ReadTime] ---------------------^

            keys = og.Controller.Keys
            controller = og.Controller()
            (_graph, nodes, _, _) = controller.edit(
                graph_path,
                {
                    keys.CREATE_NODES: [
                        ("Constant_a", "omni.graph.nodes.ConstantDouble"),  # 0
                        ("Constant_b", "omni.graph.nodes.ConstantDouble"),  # 1
                        ("Constant_c", "omni.graph.nodes.ConstantDouble"),  # 2
                        ("ReadTime", "omni.graph.nodes.ReadTime"),  # 3
                        ("Add1", "omni.graph.nodes.Add"),  # 4
                        ("Add2", "omni.graph.nodes.Add"),  # 5
                        ("Add3", "omni.graph.nodes.Add"),  # 6
                        ("Abs", "omni.graph.nodes.Magnitude"),  # 7
                    ],
                    keys.CONNECT: [
                        ("Constant_a.inputs:value", "Add1.inputs:a"),
                        ("Constant_b.inputs:value", "Add1.inputs:b"),
                        ("Add1.outputs:sum", "Add2.inputs:a"),
                        ("Constant_c.inputs:value", "Add2.inputs:b"),
                        ("Add2.outputs:sum", "Add3.inputs:a"),
                        ("ReadTime.outputs:timeSinceStart", "Add3.inputs:b"),
                        ("Add3.outputs:sum", "Abs.inputs:input"),
                    ],
                    keys.SET_VALUES: [
                        ("Constant_a.inputs:value", 1.0),
                        ("Constant_b.inputs:value", 2.0),
                        ("Constant_c.inputs:value", 3.0),
                    ],
                },
            )

            # Workaround...SET_VALUES does not set values in USD, which is needed to retain
            # values when nodes are copied
            stage = omni.usd.get_context().get_stage()
            stage.GetAttributeAtPath(graph_path + "/Constant_a.inputs:value").Set(1.0)
            stage.GetAttributeAtPath(graph_path + "/Constant_b.inputs:value").Set(2.0)
            stage.GetAttributeAtPath(graph_path + "/Constant_c.inputs:value").Set(3.0)

            return nodes

        async def validate_graph(all_nodes: Tuple[og.Node], node_indices: List[int]):

            # let the graph evaluate and cache the output value, which
            # should increase with each tick
            output_node = all_nodes[-1]
            output_attr = output_node.get_attribute("outputs:magnitude")
            await omni.kit.app.get_app().next_update_async()
            self.assertGreater(output_node.get_compute_count(), 0)
            self.assertGreater(output_attr.get(), 6.0)
            last_val = output_attr.get()

            # replace the given node set with a compound node
            all_node_paths = [Sdf.Path(node.get_prim_path()) for node in all_nodes]
            nodes = [all_nodes[i] for i in node_indices]
            node_paths = [Sdf.Path(node.get_prim_path()) for node in nodes]
            og.cmds.ReplaceWithCompound(nodes=node_paths)

            # verify the compound node was created
            compound_path = node_paths[0].GetParentPath().AppendChild("compound")
            compound_node = og.get_node_by_path(str(compound_path))
            self.assertTrue(compound_node.is_valid())

            # if the last node is not the compound
            if 7 not in node_indices:
                # re-fetch the node from a path, the previous og.node may have been invalidated
                abs_path = og.get_node_by_path(str(all_node_paths[7]))

                output_attr = abs_path.get_attribute("outputs:magnitude")
                await omni.kit.app.get_app().next_update_async()

                # note: the compute count gets reset when the replace with compound, so validate it has advanced
                self.assertGreater(abs_path.get_compute_count(), 0)
                self.assertGreater(output_attr.get(), last_val)

            else:
                self.assertGreater(compound_node.get_compute_count(), 0)

            # validate all nodes in the subgraph computed
            graph_path = compound_path.AppendChild("Graph")
            compound_graph = og.get_graph_by_path(str(graph_path))
            self.assertTrue(compound_graph.is_valid())
            compound_nodes = compound_graph.get_nodes()
            self.assertGreater(len(compound_nodes), 0)
            for compound_node in compound_nodes:
                # Quick check for constant nodes. TODO: Add a more robust method to
                # check for such nodes (check if a node has the "pure" scheduling
                # hint and only runtime-constant input attributes)?
                if "omni.graph.nodes.Constant" in compound_node.get_type_name():
                    self.assertEqual(compound_node.get_compute_count(), 0)
                else:
                    self.assertGreater(compound_node.get_compute_count(), 0)

        # add nodes
        await validate_graph(build_graph(graph_path + "0"), [4, 5, 6])

        # first two inputs + first add
        await validate_graph(build_graph(graph_path + "1"), [0, 1, 4])

        # last const + read time + last two adds
        await validate_graph(build_graph(graph_path + "2"), [2, 3, 5, 6])

        # all nodes except the last
        await validate_graph(build_graph(graph_path + "3"), [0, 1, 2, 3, 4, 5, 6])

    # -------------------------------------------------------------------------
    async def _load_node_library(self):
        stage = omni.usd.get_context().get_stage()
        root_layer = stage.GetRootLayer()
        # insert a subLayer with material.usda which contains just one material prim
        omni.kit.commands.execute(
            "CreateSublayer",
            layer_identifier=root_layer.identifier,
            sublayer_position=0,
            new_layer_path=self._node_library_test_file,
            transfer_root_content=False,
            create_or_insert=False,
        )
        await omni.kit.app.get_app().next_update_async()

    # -------------------------------------------------------------------------
    async def _unload_node_library(self):
        stage = omni.usd.get_context().get_stage()
        root_layer = stage.GetRootLayer()
        omni.kit.commands.execute("RemoveSublayer", layer_identifier=root_layer.identifier, sublayer_position=0)
        await omni.kit.app.get_app().next_update_async()

    # ------------------------------------------------------------------------
    def _validate_library_nodes_loaded(self, are_loaded: bool):
        stage = omni.usd.get_context().get_stage()
        for path in self._library_compound_paths:
            prim = stage.GetPrimAtPath(Sdf.Path(path))
            prim_loaded_and_valid = bool(prim) and prim.IsA(OmniGraphSchema.OmniGraphCompoundNodeType)
            self.assertEquals(are_loaded, prim_loaded_and_valid, f"Failed with {path}")
            self.assertEquals(are_loaded, og.get_node_type(path).is_valid())

    # -------------------------------------------------------------------------
    async def test_compound_nodes_load_from_layer(self):
        """
        Tests that node libraries loaded from a separate layer correctly
        load and unload
        """

        # load the library
        await self._load_node_library()
        self._validate_library_nodes_loaded(True)

        # unload
        await self._unload_node_library()
        self._validate_library_nodes_loaded(False)

        # reload the library
        await self._load_node_library()
        self._validate_library_nodes_loaded(True)

        # unload the whole stage
        await omni.usd.get_context().new_stage_async()
        self._validate_library_nodes_loaded(False)

    # -----------------------------------------------------------------------
    async def test_compound_node_can_reference_layer(self):
        """
        Tests that a compound node that references a compound graph
        located in a layer
        """

        await self._load_node_library()

        # builds the graph
        #   [const]-->[compound]------->[magnitude]
        #   [const]---^  ^
        #   [const]------|

        keys = og.Controller.Keys
        controller = og.Controller()
        (_graph, nodes, _, _) = controller.edit(
            self._test_graph_path,
            {
                keys.CREATE_NODES: [
                    ("Constant_a", "omni.graph.nodes.ConstantDouble"),
                    ("Constant_b", "omni.graph.nodes.ConstantDouble"),
                    ("Constant_c", "omni.graph.nodes.ConstantDouble"),
                    ("Madd", "/World/Compounds/Madd"),
                    ("Abs", "omni.graph.nodes.Magnitude"),
                ],
                keys.CONNECT: [
                    ("Constant_a.inputs:value", "Madd.inputs:a"),
                    ("Constant_b.inputs:value", "Madd.inputs:b"),
                    ("Constant_c.inputs:value", "Madd.inputs:c"),
                    ("Madd.outputs:sum", "Abs.inputs:input"),
                ],
                keys.SET_VALUES: [
                    ("Constant_a.inputs:value", 1.0),
                    ("Constant_b.inputs:value", 2.0),
                    ("Constant_c.inputs:value", 3.0),
                ],
            },
        )

        await omni.kit.app.get_app().next_update_async()
        for node in nodes:
            # Quick check for constant nodes. TODO: Add a more robust method to
            # check for such nodes (check if a node has the "pure" scheduling
            # hint and only runtime-constant input attributes)?
            if not node.is_compound_node() and "omni.graph.nodes.Constant" not in node.get_type_name():
                self.assertGreater(node.get_compute_count(), 0, f"Expected {node.get_prim_path()} to be computed")

        attr = nodes[4].get_attribute("outputs:magnitude")
        self.assertEquals(attr.get(), 1.0 * 2.0 + 3.0)

    # -------------------------------------------------------------------------
    async def test_compound_from_reference_runs(self):
        """
        Tests that a graph containing a compound node with an external reference
        can be loaded and run
        """
        # Loads a graph on disk that looks like:
        #   where the compound references a graph from the TestNodeLibrary.usda (madd)
        #   [const]-->[compound]------->[magnitude]
        #   [const]---^  ^
        #   [const]------|
        (result, error) = await ogts.load_test_file(self._test_file_with_reference, use_caller_subdirectory=True)
        self.assertTrue(result, error)

        await omni.kit.app.get_app().next_update_async()

        graph = og.get_graph_by_path("/World/PushGraph")
        nodes = graph.get_nodes()
        self.assertGreater(len(nodes), 4, "Expected at least 5 nodes")
        for node in nodes:
            # Quick check for constant nodes. TODO: Add a more robust method to
            # check for such nodes (check if a node has the "pure" scheduling
            # hint and only runtime-constant input attributes)?
            if not node.is_compound_node() and "omni.graph.nodes.Constant" not in node.get_type_name():
                self.assertGreater(node.get_compute_count(), 0, f"Expected {node.get_prim_path()} to be computed")

        output = og.get_node_by_path("/World/PushGraph/magnitude")
        attr = output.get_attribute("outputs:magnitude")
        self.assertEquals(attr.get(), 1.0 * 2.0 + 3.0)

    # -------------------------------------------------------------------------
    async def test_nested_reference_compound(self):
        """
        Tests that a nested compound node loaded from a compound library runs
        """

        await self._load_node_library()

        keys = og.Controller.Keys
        controller = og.Controller()
        (_graph, (_constant, convert), _, _) = controller.edit(
            self._test_graph_path,
            {
                keys.CREATE_NODES: [
                    ("Constant_a", "omni.graph.nodes.ConstantDouble"),
                    ("Convert", "/World/Compounds/CelciusToFahrenheit"),
                ],
                keys.CONNECT: [
                    ("Constant_a.inputs:value", "Convert.inputs:a"),
                ],
                keys.SET_VALUES: [
                    ("Constant_a.inputs:value", 1.0),
                ],
            },
        )

        await omni.kit.app.get_app().next_update_async()

        self.assertEquals(convert.get_attribute("outputs:value").get(), 33.8)

    # -------------------------------------------------------------------------
    async def test_fan_out_to_compounds(self):
        """Tests that fan out into multiple compound nodes"""

        await self._load_node_library()

        # [Const] -> [Compound] -> [Add]
        #         -> [Compound] ->

        keys = og.Controller.Keys
        controller = og.Controller()
        (_, (_, compound_a, compound_b, add), _, _) = controller.edit(
            self._test_graph_path,
            {
                keys.CREATE_NODES: [
                    ("Constant_a", "omni.graph.nodes.ConstantDouble"),
                    ("Convert_a", "/World/Compounds/Deg2Rad"),
                    ("Convert_b", "/World/Compounds/Deg2Rad"),
                    ("Add", "omni.graph.nodes.Add"),
                ],
                keys.CONNECT: [
                    ("Constant_a.inputs:value", "Convert_a.inputs:a"),
                    ("Constant_a.inputs:value", "Convert_b.inputs:a"),
                    ("Convert_a.outputs:result", "Add.inputs:a"),
                    ("Convert_b.outputs:result", "Add.inputs:b"),
                ],
                keys.SET_VALUES: [
                    ("Constant_a.inputs:value", 180),
                ],
            },
        )

        await omni.kit.app.get_app().next_update_async()

        self.assertEquals(compound_a.get_attribute("outputs:result").get(), math.pi)
        self.assertEquals(compound_b.get_attribute("outputs:result").get(), math.pi)
        self.assertEquals(add.get_attribute("outputs:sum").get(), math.pi * 2.0)

    # -------------------------------------------------------------------------
    async def test_fan_out_from_compounds(self):
        """Tests that fan out from a compound node works"""

        await self._load_node_library()

        # [Const] -> [Compound] -> [Abs]
        #                       -> [Abs]

        keys = og.Controller.Keys
        controller = og.Controller()
        (_, (_, _, abs_a, abs_b), _, _) = controller.edit(
            self._test_graph_path,
            {
                keys.CREATE_NODES: [
                    ("Constant_a", "omni.graph.nodes.ConstantDouble"),
                    ("Convert_a", "/World/Compounds/Deg2Rad"),
                    ("Abs_a", "omni.graph.nodes.Magnitude"),
                    ("Abs_b", "omni.graph.nodes.Magnitude"),
                ],
                keys.CONNECT: [
                    ("Constant_a.inputs:value", "Convert_a.inputs:a"),
                    ("Convert_a.outputs:result", "Abs_a.inputs:input"),
                    ("Convert_a.outputs:result", "Abs_b.inputs:input"),
                ],
                keys.SET_VALUES: [("Constant_a.inputs:value", 180)],
            },
        )

        await omni.kit.app.get_app().next_update_async()

        self.assertEquals(abs_a.get_attribute("outputs:magnitude").get(), math.pi)
        self.assertEquals(abs_b.get_attribute("outputs:magnitude").get(), math.pi)

    # -------------------------------------------------------------------------
    async def test_register_compound_with_node_type_conflict(self):
        """
        Tests that compound nodes registered with the same as built-in nodes produces error
        and does NOT override the default node usage
        """

        # Create the compound node
        with ogts.ExpectedError():
            self._create_test_compound("Add", "Graph", "omni.graph.nodes")

        # Validate the compound is still created
        stage = omni.usd.get_context().get_stage()
        self.assertTrue(stage.GetPrimAtPath(self.get_compound_folder() + "/Add_01").IsValid())
        self.assertTrue(stage.GetPrimAtPath(self.get_compound_folder() + "/Add_01/Graph").IsValid())

        # Validate the built-in node type is actually used
        keys = og.Controller.Keys
        controller = og.Controller()
        (graph, (_, _, add), _, _) = controller.edit(
            self._test_graph_path,
            {
                keys.CREATE_NODES: [
                    ("Constant_a", "omni.graph.nodes.ConstantDouble"),
                    ("Constant_b", "omni.graph.nodes.ConstantDouble"),
                    ("Add", "omni.graph.nodes.Add"),
                ],
                keys.CONNECT: [
                    ("Constant_a.inputs:value", "Add.inputs:a"),
                    ("Constant_b.inputs:value", "Add.inputs:b"),
                ],
                keys.SET_VALUES: [("Constant_a.inputs:value", 1), ("Constant_b.inputs:value", 2)],
            },
        )

        await controller.evaluate(graph)
        self.assertEquals(add.get_attribute("outputs:sum").get(), 3)

    # -------------------------------------------------------------------------

    async def test_register_compound_with_compound_conflict(self):
        """Tests errors when registering multiple compounds with the same name"""
        self._create_test_compound("Compound")
        with ogts.ExpectedError():
            self._create_test_compound(compound_name="Compound", compound_dir=Sdf.Path("/World/Compound/SubDir"))
        self._create_and_test_graph_from_add_compound("local.nodes.Compound")

    # -------------------------------------------------------------------------

    async def test_create_node_type_prevents_duplicates(self):
        """Tests that CreateNodeType prevents duplicates through renaming"""

        ogu.cmds.CreateCompoundNodeType(compound_name="Compound", graph_name="Graph")
        ogu.cmds.CreateCompoundNodeType(compound_name="Compound", graph_name="Graph")
        ogu.cmds.CreateCompoundNodeType(compound_name="Add", graph_name="Graph", namespace="omni.graph.nodes")

        stage = omni.usd.get_context().get_stage()
        folder = stage.GetPrimAtPath(self.get_compound_folder())
        self.assertEquals(len(folder.GetChildren()), 3)
        self.assertTrue(stage.GetPrimAtPath(self.get_compound_folder() + "/Compound").IsValid())
        self.assertFalse(stage.GetPrimAtPath(self.get_compound_folder() + "/Add").IsValid())

    # --------------------------------------------------------------------------

    async def test_create_node_type_with_existing_node_name(self):
        """
        Tests that creating a node type with a name conflict on an existing node in the
        graph functions does not break
        """

        keys = og.Controller.Keys
        controller = og.Controller()
        (graph, (const_a, const_b, _, add, add_01), _, _) = controller.edit(
            self._test_graph_path,
            {
                keys.CREATE_NODES: [
                    ("Constant_a", "omni.graph.nodes.ConstantDouble"),
                    ("Constant_b", "omni.graph.nodes.ConstantDouble"),
                    ("Constant_c", "omni.graph.nodes.ConstantDouble"),
                    ("Add", "omni.graph.nodes.Add"),
                    ("Add_01", "omni.graph.nodes.Add"),
                ],
                keys.CONNECT: [
                    ("Constant_a.inputs:value", "Add.inputs:a"),
                    ("Constant_b.inputs:value", "Add.inputs:b"),
                    ("Add.outputs:sum", "Add_01.inputs:a"),
                    ("Constant_c.inputs:value", "Add_01.inputs:b"),
                ],
                keys.SET_VALUES: [
                    ("Constant_a.inputs:value", 1.0),
                    ("Constant_b.inputs:value", 2.0),
                    ("Constant_c.inputs:value", 3.0),
                ],
            },
        )

        await og.Controller.evaluate(graph)
        self.assertEquals(add_01.get_attribute("outputs:sum").get(), 6.0)

        # replace the nodes with a node type that has the same name as an existing node
        node_paths = [Sdf.Path(node.get_prim_path()) for node in [const_a, const_b, add]]
        og.cmds.ReplaceWithCompound(nodes=node_paths, compound_name="Add_01")

        # validate
        add_01 = get_node(graph, "Add_01")
        compound_node = next(filter(lambda node: node.is_compound_node(), graph.get_nodes()), None)
        self.assertFalse(get_node(graph, "Constant_a").is_valid())
        self.assertFalse(get_node(graph, "Constant_b").is_valid())
        self.assertTrue(get_node(graph, "Constant_c").is_valid())
        self.assertTrue(add_01.is_valid())
        self.assertIsNotNone(compound_node)
        self.assertNotEquals(Sdf.Path(compound_node.get_prim_path()).name, "Add_01")

        # validate the graph
        await og.Controller.evaluate(graph)
        self.assertEquals(add_01.get_attribute("outputs:sum").get(), 6.0)

    # --------------------------------------------------------------------------

    async def test_is_compound_node_type(self):
        """Simple test that validates nodetype.is_compound_node"""

        await self._load_node_library()

        # C++ nodes
        self.assertFalse(og.get_node_type("omni.graph.test.Add2IntegerArrays").is_compound_node_type())
        self.assertFalse(og.get_node_type("omni.graph.test.ComputeErrorCpp").is_compound_node_type())

        # python nodes
        self.assertFalse(og.get_node_type("omni.graph.test.ComputeErrorPy").is_compound_node_type())
        self.assertFalse(og.get_node_type("omni.graph.test.TestAllDataTypesPy").is_compound_node_type())

        # compound nodes
        self.assertTrue(og.get_node_type("local.nodes.Madd").is_compound_node_type())
        self.assertTrue(og.get_node_type("local.nodes.DebugString").is_compound_node_type())
        self.assertTrue(og.get_node_type("local.nodes.Deg2Rad").is_compound_node_type())
        self.assertTrue(og.get_node_type("local.nodes.CelciusToFahrenheit").is_compound_node_type())

    # --------------------------------------------------------------------------

    async def test_compound_nodes_trigger_graph_registry_events(self):
        """Tests that changing compound node values triggers appropriate graph registry events"""
        node_type_t = OmniGraphSchema.OmniGraphCompoundNodeType
        await self._load_node_library()
        events = []

        def on_changed(event):
            nonlocal events
            events.append(event)

        sub = og.GraphRegistry().get_event_stream().create_subscription_to_pop(on_changed)
        self.assertIsNotNone(sub)

        stage = omni.usd.get_context().get_stage()
        prims = [node_type_t(stage.GetPrimAtPath(path)) for path in self._library_compound_paths]
        for prim in prims:
            prim.GetOmniGraphCategoriesAttr().Set(["NewCategory"])

        await omni.kit.app.get_app().next_update_async()
        self.assertEquals(len(events), len(self._library_compound_paths))
        self.assertTrue(all(e.type == int(og.GraphRegistryEvent.NODE_TYPE_CATEGORY_CHANGED) for e in events))

        events = []
        for prim in prims:
            prim.GetOmniGraphNamespaceAttr().Set("other.namespace")
        await omni.kit.app.get_app().next_update_async()
        # Double the count because there will be two events per compound (node type added, namespace changed)
        self.assertEquals(len(events), len(self._library_compound_paths) * 2)
        add_events = [
            int(og.GraphRegistryEvent.NODE_TYPE_NAMESPACE_CHANGED),
            int(og.GraphRegistryEvent.NODE_TYPE_ADDED),
        ]
        self.assertTrue(all(e.type in add_events for e in events))

        sub = None

    # --------------------------------------------------------------------------

    async def test_is_compound_graph(self):
        """Tests is_compound_graph returns true for compounds graphs"""

        (result, error) = await ogts.load_test_file(self._test_file_with_reference, use_caller_subdirectory=True)
        self.assertTrue(result, error)

        await omni.kit.app.get_app().next_update_async()

        graph = og.get_graph_by_path("/World/PushGraph")
        self.assertTrue(graph.is_valid())
        self.assertFalse(graph.is_compound_graph())

        graph = og.get_graph_by_path("/World/PushGraph/madd/Graph")
        self.assertTrue(graph.is_valid())
        self.assertTrue(graph.is_compound_graph())

    # ---------------------------------------------------------------------

    def _create_lazy_compound(
        self, compound_name, evaluator_type, graph_name="Graph", namespace="local.nodes.", compound_dir=None
    ):
        """Creates a compound node/graph that wraps an add node
        The output is the time from a ReadTime node
        """
        # Create a compound node/graph that can be used to evaluate lazy graphs
        (success, schema_prim) = ogu.cmds.CreateCompoundNodeType(
            compound_name=compound_name,
            graph_name=graph_name,
            namespace=namespace,
            folder=compound_dir,
            evaluator_type=evaluator_type,
        )
        if not success:
            raise og.OmniGraphError("CreateNodeType failed")

        compound_path = str(schema_prim.GetPrim().GetPath())
        graph_path = f"{compound_path}/{graph_name}"
        node_path = f"{graph_path}/ReadTime_Node"
        node_type = get_node_type_from_schema_prim(schema_prim)

        # Create a node in the sub graph
        og.cmds.CreateNode(
            graph=og.get_graph_by_path(graph_path),
            node_path=node_path,
            node_type="omni.graph.nodes.ReadTime",
            create_usd=True,
        )

        # assign inputs and outputs
        ogu.cmds.CreateCompoundNodeTypeOutput(
            node_type=node_type, output_name="value", attribute_path=Sdf.Path(f"{node_path}.outputs:timeSinceStart")
        )

    # -------------------------------------------------------------------------

    def _create_evaluation_graph(
        self, compound_name, evaluator_type, graph_name="Graph", namespace="local.nodes.", compound_dir=None
    ):
        """
        Creates a compound node for an evaluation graph
        The underlying graph is [OnImpulse]->[TickCounter] and outputs the tick count
        """
        (success, schema_prim) = ogu.cmds.CreateCompoundNodeType(
            compound_name=compound_name,
            graph_name=graph_name,
            namespace=namespace,
            folder=compound_dir,
            evaluator_type=evaluator_type,
        )
        if not success:
            raise og.OmniGraphError("CreateNodeType failed")

        compound_path = str(schema_prim.GetPrim().GetPath())
        graph_path = f"{compound_path}/{graph_name}"
        node_path = f"{graph_path}/TickCounter"
        node_type = get_node_type_from_schema_prim(schema_prim)

        keys = og.Controller.Keys
        controller = og.Controller()
        (_, (_impulse, _counter), _, _) = controller.edit(
            graph_path,
            {
                keys.CREATE_NODES: [
                    ("OnImpulse", "omni.graph.action.OnImpulseEvent"),
                    ("TickCounter", "omni.graph.action.Counter"),
                ],
                keys.CONNECT: [
                    ("OnImpulse.outputs:execOut", "TickCounter.inputs:execIn"),
                ],
                keys.SET_VALUES: [("OnImpulse.inputs:onlyPlayback", False), ("OnImpulse.state:enableImpulse", True)],
            },
        )

        # assign inputs and outputs
        ogu.cmds.CreateCompoundNodeTypeOutput(
            node_type=node_type, output_name="value", attribute_path=Sdf.Path(f"{node_path}.outputs:count")
        )

    # -------------------------------------------------------------------------

    async def __evaluate_graph_types(self, compound_type: str, runner_type: str):
        """Tests that graphs can be referenced using a different namespace"""

        if compound_type == "execution":
            self._create_evaluation_graph("Compound", compound_type)
        else:
            self._create_lazy_compound("Compound", compound_type)

        # create a graph that uses the compound graph
        keys = og.Controller.Keys
        controller = og.Controller()
        (graph, (_compound, abs_node), _, _) = controller.edit(
            {"graph_path": self._test_graph_path, "evaluator_name": runner_type},
            {
                keys.CREATE_NODES: [
                    ("Compound", f"{self.get_compound_folder()}/Compound"),
                    ("Abs", "omni.graph.nodes.Magnitude"),
                ],
                keys.CONNECT: [
                    ("Compound.outputs:value", "Abs.inputs:input"),
                ],
            },
        )

        await omni.kit.app.get_app().next_update_async()
        await og.Controller.evaluate(graph)

        result = abs_node.get_attribute("outputs:magnitude").get()
        return result > 0.0

    # -------------------------------------------------------------------------

    async def test_intermix_evaluator_types_in_compounds(self):
        """
        Tests mixing evaluator types of graphs with compounds

        This test uses a compound with a given evaluator type and a single output connected
        to an outer graph
        """
        # play the timeline so readtime nodes get updated
        omni.timeline.get_timeline_interface().play()

        outer_graph_modes = ["push", "dirty_push"]
        compound_modes = ["push", "dirty_push", "execution"]
        for (graph_type, compound_type) in itertools.product(outer_graph_modes, compound_modes):
            with self.subTest(compound_type=compound_type, graph_type=graph_type):
                success = await self.__evaluate_graph_types(compound_type, graph_type)
                self.assertTrue(success, f"Graph Type: '{graph_type}' with Compound Type: '{compound_type}'")
            await omni.usd.get_context().new_stage_async()

    # -------------------------------------------------------------------------

    async def test_evaluator_types_in_compounds(self):
        """
        Test different evaluator types of graphs with compounds that have inputs and outputs
        where the outer graph is a push graph
        """
        compound_modes = ["push", "dirty_push", "execution"]
        for compound_type in compound_modes:
            with self.subTest(compound_type=compound_type):
                self._create_test_compound("Compound", evaluator_type=compound_type)
                should_add = compound_type in ("push", "dirty_push")
                self._create_and_test_graph_from_add_compound(
                    f"{self.get_compound_folder()}/Compound", should_add=should_add
                )
            await omni.usd.get_context().new_stage_async()

    # ----------------------------------------------------------------------------

    async def test_deleted_compound_graph(self):
        """Basic test that validates a deleted compound doesn't cause a crash"""
        stage = omni.usd.get_context().get_stage()

        self._create_test_compound("Compound")
        graph = self._create_and_test_graph_from_add_compound("local.nodes.Compound")

        omni.kit.commands.execute("DeletePrims", paths=[self.get_compound_path()])
        self.assertFalse(stage.GetPrimAtPath(self.get_compound_path()).IsValid())

        graph.evaluate()

    # ----------------------------------------------------------------------------

    async def test_create_node_type_attributes_with_fixed_types(self):
        """
        Tests the CreateNodeTypeInput and CreateNodeTypeOutput with predefined types, including
        extended types and validates the attributes are correctly created on the resulting
        node instances
        """
        stage = omni.usd.get_context().get_stage()
        (_, schema_prim) = ogu.cmds.CreateCompoundNodeType(compound_name="Compound", graph_name="Graph")
        self.assertTrue(stage.GetPrimAtPath(self.get_compound_path()).IsValid())

        node_type = get_node_type_from_schema_prim(schema_prim)

        og_types = [
            og.AttributeType.type_from_ogn_type_name(type_name) for type_name in ogn.supported_attribute_type_names()
        ]
        str_inputs = ogn.supported_attribute_type_names() + list(ogn.ATTRIBUTE_UNION_GROUPS.keys())

        # create attributes both from og.Type and ogn type strings (including "union" types)
        for idx, og_type in enumerate(og_types):
            name = re.sub(r"\W", "_", og_type.get_ogn_type_name())
            ogu.cmds.CreateCompoundNodeTypeInput(
                node_type=node_type, input_name=f"input_{name}_{idx}", attribute_path=None, default_type=og_type
            )
            if og_type.role not in [og.AttributeRole.BUNDLE, og.AttributeRole.TARGET]:
                ogu.cmds.CreateCompoundNodeTypeOutput(
                    node_type=node_type, output_name=f"output_{name}_{idx}", attribute_path=None, default_type=og_type
                )
        for idx, str_type in enumerate(str_inputs):
            name = re.sub(r"\W", "_", str_type)
            ogu.cmds.CreateCompoundNodeTypeInput(
                node_type=node_type, input_name=f"input_s_{name}_{idx}", attribute_path=None, default_type=str_type
            )
            if str_type not in ["bundle", "target"]:
                ogu.cmds.CreateCompoundNodeTypeOutput(
                    node_type=node_type,
                    output_name=f"output_s_{name}_{idx}",
                    attribute_path=None,
                    default_type=str_type,
                )

        # validate bundles don't work
        omni.kit.commands.set_logging_enabled(False)
        (result, _) = ogu.cmds.CreateCompoundNodeTypeOutput(
            node_type=node_type, output_name="output_bundle_a", attribute_path=None, default_type="bundle"
        )
        self.assertFalse(result)
        (result, _) = ogu.cmds.CreateCompoundNodeTypeOutput(
            node_type=node_type,
            output_name="output_bundle_b",
            attribute_path=None,
            default_type=og.Type(og.BaseDataType.PRIM, 1, 0, og.AttributeRole.BUNDLE),
        )
        self.assertFalse(result)
        omni.kit.commands.set_logging_enabled(True)

        # validate specifying no path or default_type still creates inputs and outputs
        ogu.cmds.CreateCompoundNodeTypeInput(
            node_type=node_type, input_name="input_empty", attribute_path=None, default_type=None
        )
        ogu.cmds.CreateCompoundNodeTypeOutput(
            node_type=node_type, output_name="output_empty", attribute_path=None, default_type=None
        )

        # validate creation with list style unions
        float_type_subset = [
            og.Type(og.BaseDataType.FLOAT),
            og.Type(og.BaseDataType.FLOAT, 2),
            og.Type(og.BaseDataType.FLOAT, 3),
        ]
        ogu.cmds.CreateCompoundNodeTypeInput(
            node_type=node_type, input_name="input_union", attribute_path=None, default_type=float_type_subset
        )
        ogu.cmds.CreateCompoundNodeTypeOutput(
            node_type=node_type, output_name="output_union", attribute_path=None, default_type=float_type_subset
        )

        # create a graph that uses the compound graph
        (_, compound_node) = self._create_graph_with_compound_node(f"{self.get_compound_folder()}/Compound")

        # validate the attributes on the node have been created, and the resolved type matches where there is a regular type
        for idx, og_type in enumerate(og_types):
            name = re.sub(r"\W", "_", og_type.get_ogn_type_name())
            input_name = f"inputs:input_{name}_{idx}"
            self.assertTrue(compound_node.get_attribute(input_name).is_valid(), input_name)
            self.assertEquals(compound_node.get_attribute(input_name).get_resolved_type(), og_type)
            if og_type.role not in [og.AttributeRole.BUNDLE, og.AttributeRole.TARGET]:
                output_name = f"outputs:output_{name}_{idx}"
                self.assertTrue(compound_node.get_attribute(output_name).is_valid(), output_name)
                self.assertEquals(compound_node.get_attribute(output_name).get_resolved_type(), og_type)

        for idx, str_type in enumerate(str_inputs):
            name = re.sub(r"\W", "_", str_type)
            input_name = f"inputs:input_s_{name}_{idx}"
            self.assertTrue(compound_node.get_attribute(input_name).is_valid(), input_name)
            if str_type not in ["bundle", "target"]:
                output_name = f"outputs:output_s_{name}_{idx}"
                self.assertTrue(compound_node.get_attribute(output_name).is_valid(), output_name)

        # empty inputs and outputs create
        self.assertTrue(compound_node.get_attribute("inputs:input_empty").is_valid())
        self.assertTrue(compound_node.get_attribute("outputs:output_empty").is_valid())

        # list-style unions
        self.assertTrue(compound_node.get_attribute("inputs:input_union").is_valid())
        self.assertEquals(
            compound_node.get_attribute("inputs:input_union").get_resolved_type(), og.Type(og.BaseDataType.UNKNOWN)
        )
        self.assertTrue(compound_node.get_attribute("outputs:output_union").is_valid())
        self.assertEquals(
            compound_node.get_attribute("outputs:output_union").get_resolved_type(), og.Type(og.BaseDataType.UNKNOWN)
        )

    # ----------------------------------------------------------------------------

    async def test_create_node_type_attributes_with_fixed_types_and_connections(self):
        """
        Tests CreateNodeTypeInput and CreateNodeTypeOutput when both an attribute_path
        and a fixed types are applied
        """
        # create a compound node
        stage = omni.usd.get_context().get_stage()
        (_, schema_prim) = ogu.cmds.CreateCompoundNodeType(compound_name="Compound", graph_name="Graph")
        self.assertTrue(stage.GetPrimAtPath(self.get_compound_path()).IsValid())

        node_path = f"{self.get_compound_graph_path()}/Node"
        union_path = f"{self.get_compound_graph_path()}/UnionNode"
        union_path_2 = f"{self.get_compound_graph_path()}/UnionNodeTwo"
        node_type = get_node_type_from_schema_prim(schema_prim)
        input_prefix = f"{node_path}.inputs:"
        output_prefix = f"{node_path}.outputs:"

        # Create a node in the sub graph that has many fixed types
        og.cmds.CreateNode(
            graph=og.get_graph_by_path(self.get_compound_graph_path()),
            node_path=node_path,
            node_type="omni.graph.test.TestAllDataTypes",
            create_usd=True,
        )

        # And nodes with union types
        for path in [union_path, union_path_2]:
            og.cmds.CreateNode(
                graph=og.get_graph_by_path(self.get_compound_graph_path()),
                node_path=path,
                node_type="omni.graph.nodes.Add",
                create_usd=True,
            )

        # attributes where the types match
        ogu.cmds.CreateCompoundNodeTypeInput(
            node_type=node_type,
            input_name="input_bool",
            attribute_path=Sdf.Path(f"{input_prefix}a_bool"),
            default_type="bool",
        )
        ogu.cmds.CreateCompoundNodeTypeOutput(
            node_type=node_type,
            output_name="output_bool",
            attribute_path=Sdf.Path(f"{output_prefix}a_bool"),
            default_type="bool",
        )

        # attributes where the defined type is a union, and the attribute type is not
        ogu.cmds.CreateCompoundNodeTypeInput(
            node_type=node_type,
            input_name="input_float",
            attribute_path=Sdf.Path(f"{input_prefix}a_float"),
            default_type="numerics",
        )
        ogu.cmds.CreateCompoundNodeTypeOutput(
            node_type=node_type,
            output_name="output_float",
            attribute_path=Sdf.Path(f"{output_prefix}a_float"),
            default_type="numerics",
        )

        # attributes where the defined type is fixed, and the attribute type is a union
        ogu.cmds.CreateCompoundNodeTypeInput(
            node_type=node_type,
            input_name="input_double",
            attribute_path=Sdf.Path(f"{union_path}.inputs:a"),
            default_type="double",
        )
        ogu.cmds.CreateCompoundNodeTypeOutput(
            node_type=node_type,
            output_name="output_double",
            attribute_path=Sdf.Path(f"{union_path}.outputs:sum"),
            default_type="double",
        )

        # attributes where the types don't match
        with ogts.ExpectedError():
            ogu.cmds.CreateCompoundNodeTypeInput(
                node_type=node_type,
                input_name="input_float_2",
                attribute_path=Sdf.Path(f"{input_prefix}a_token"),
                default_type="float[2]",
            )
        with ogts.ExpectedError():
            ogu.cmds.CreateCompoundNodeTypeOutput(
                node_type=node_type,
                output_name="output_float_2",
                attribute_path=Sdf.Path(f"{output_prefix}a_token"),
                default_type="float[2]",
            )

        # union to union
        ogu.cmds.CreateCompoundNodeTypeInput(
            node_type=node_type,
            input_name="input_union",
            attribute_path=Sdf.Path(f"{union_path}.inputs:b"),
            default_type="numerics",
        )
        ogu.cmds.CreateCompoundNodeTypeOutput(
            node_type=node_type,
            output_name="output_union",
            attribute_path=Sdf.Path(f"{union_path_2}.outputs:sum"),
            default_type="numerics",
        )

        # create a graph that uses the compound graph
        (_, compound_node) = self._create_graph_with_compound_node(f"{self.get_compound_folder()}/Compound")

        # types match exactly
        self.assertTrue(compound_node.get_attribute("inputs:input_bool").is_valid())
        self.assertEquals(
            compound_node.get_attribute("inputs:input_bool").get_resolved_type(), og.Type(og.BaseDataType.BOOL)
        )
        self.assertTrue(compound_node.get_attribute("outputs:output_bool").is_valid())
        self.assertEquals(
            compound_node.get_attribute("outputs:output_bool").get_resolved_type(), og.Type(og.BaseDataType.BOOL)
        )

        # definition is a union, target is fixed
        self.assertTrue(compound_node.get_attribute("inputs:input_float").is_valid())
        self.assertEquals(
            compound_node.get_attribute("inputs:input_float").get_resolved_type(), og.Type(og.BaseDataType.UNKNOWN)
        )
        self.assertTrue(compound_node.get_attribute("outputs:output_float").is_valid())
        self.assertEquals(
            compound_node.get_attribute("outputs:output_float").get_resolved_type(), og.Type(og.BaseDataType.FLOAT)
        )

        # definition is fixed, target is a union
        self.assertTrue(compound_node.get_attribute("inputs:input_double").is_valid())
        self.assertEquals(
            compound_node.get_attribute("inputs:input_double").get_resolved_type(), og.Type(og.BaseDataType.DOUBLE)
        )
        self.assertTrue(compound_node.get_attribute("outputs:output_double").is_valid())
        self.assertEquals(
            compound_node.get_attribute("outputs:output_double").get_resolved_type(), og.Type(og.BaseDataType.DOUBLE)
        )

        # attributes where the types don't match
        self.assertTrue(compound_node.get_attribute("inputs:input_float_2").is_valid())
        self.assertEquals(
            compound_node.get_attribute("inputs:input_float_2").get_resolved_type(),
            og.Type(og.BaseDataType.FLOAT, 2, 0),
        )
        self.assertTrue(compound_node.get_attribute("outputs:output_float_2").is_valid())
        self.assertEquals(
            compound_node.get_attribute("outputs:output_float_2").get_resolved_type(),
            og.Type(og.BaseDataType.FLOAT, 2, 0),
        )

        # both are unions
        self.assertTrue(compound_node.get_attribute("inputs:input_union").is_valid())
        self.assertEquals(
            compound_node.get_attribute("inputs:input_union").get_resolved_type(), og.Type(og.BaseDataType.UNKNOWN)
        )
        self.assertTrue(compound_node.get_attribute("outputs:output_union").is_valid())
        self.assertEquals(
            compound_node.get_attribute("outputs:output_union").get_resolved_type(), og.Type(og.BaseDataType.UNKNOWN)
        )

    # ----------------------------------------------------------------------------
    async def test_compound_node_input_output_metadata(self):
        """
        Validate that metadata created on the input/outputs of a Compound Node Type
        gets transferred to created nodes from those compounds
        """
        stage = omni.usd.get_context().get_stage()
        (_, schema_prim) = ogu.cmds.CreateCompoundNodeType(compound_name="Compound", graph_name="Graph")
        self.assertTrue(stage.GetPrimAtPath(self.get_compound_path()).IsValid())

        node_type = get_node_type_from_schema_prim(schema_prim)
        ogu.cmds.CreateCompoundNodeTypeInput(
            node_type=node_type,
            input_name="input_a",
            attribute_path=None,
            default_type="int",
        )
        ogu.cmds.CreateCompoundNodeTypeOutput(
            node_type=node_type,
            output_name="output_a",
            attribute_path=None,
            default_type="int",
        )

        # get the relationship created as the type input and output
        input_rel = schema_prim.GetPrim().GetRelationship("omni:graph:input:input_a")
        output_rel = schema_prim.GetPrim().GetRelationship("omni:graph:output:output_a")
        self.assertTrue(input_rel.IsValid())
        self.assertTrue(output_rel.IsValid())

        # set the metadata on the relationships.
        input_rel.SetCustomDataByKey(DESCRIPTION_METADATA_KEY, "InputDescription")
        input_rel.SetCustomDataByKey(DISPLAYNAME_METADATA_KEY, "InputDisplayName")
        output_rel.SetCustomDataByKey(DESCRIPTION_METADATA_KEY, "OutputDescription")
        output_rel.SetCustomDataByKey(DISPLAYNAME_METADATA_KEY, "OutputDisplayName")

        # create a graph that uses the compound node type
        keys = og.Controller.Keys
        controller = og.Controller()
        (_, nodes, _, _) = controller.edit(
            self._test_graph_path,
            {
                keys.CREATE_NODES: [
                    ("Compound", f"{self.get_compound_folder()}/Compound"),
                ]
            },
        )
        node = nodes[0]

        # Validate that the resulting node contains the metadata transfered when the node is created
        input_attr = node.get_attribute("inputs:input_a")
        output_attr = node.get_attribute("outputs:output_a")

        self.assertEquals("InputDescription", input_attr.get_metadata(ogn.MetadataKeys.DESCRIPTION))
        self.assertEquals("InputDisplayName", input_attr.get_metadata(ogn.MetadataKeys.UI_NAME))
        self.assertEquals("OutputDescription", output_attr.get_metadata(ogn.MetadataKeys.DESCRIPTION))
        self.assertEquals("OutputDisplayName", output_attr.get_metadata(ogn.MetadataKeys.UI_NAME))

    # -------------------------------------------------------------------------
    def assert_attribute_value(self, actual, expected):
        """
        Helper method that deals with values returned from attributes, where
        lists may actually be numpy arrays.
        """
        ogts.verify_values(expected, actual, f"Comparison failed {str(actual)} vs {str(expected)}")

    # ----------------------------------------------------------------------------
    async def test_compound_node_input_defaults(self):
        """
        Test that validates compound nodes that carry default values in their metadata initialize nodes
        with the default values. This validates the attribute is correctly loaded with the typed default value
        in the metadata of node type definition
        """
        (result, error) = await ogts.load_test_file(self._test_file_with_default_values, use_caller_subdirectory=True)
        self.assertTrue(result, error)

        compound_node_type = ogu.find_compound_node_type_by_path("/World/Compounds/Compound")

        self.assert_attribute_value(compound_node_type.find_input("a_bool").get_default_data(), 0)
        self.assert_attribute_value(compound_node_type.find_input("a_bool_array").get_default_data(), [0, 1])
        self.assert_attribute_value(
            compound_node_type.find_input("a_colord_3").get_default_data(), [0.01625, 0.14125, 0.26625]
        )
        self.assert_attribute_value(
            compound_node_type.find_input("a_colord_3_array").get_default_data(),
            [[0.01625, 0.14125, 0.26625], [0.26625, 0.14125, 0.01625]],
        )
        self.assert_attribute_value(
            compound_node_type.find_input("a_colord_4").get_default_data(), [0.01625, 0.14125, 0.26625, 0.39125]
        )
        self.assert_attribute_value(
            compound_node_type.find_input("a_colord_4_array").get_default_data(),
            [[0.01625, 0.14125, 0.26625, 0.39125], [0.39125, 0.26625, 0.14125, 0.01625]],
        )
        self.assert_attribute_value(
            compound_node_type.find_input("a_colorf_3").get_default_data(), [0.125, 0.25, 0.375]
        )
        self.assert_attribute_value(
            compound_node_type.find_input("a_colorf_3_array").get_default_data(),
            [[0.125, 0.25, 0.375], [0.375, 0.25, 0.125]],
        )
        self.assert_attribute_value(
            compound_node_type.find_input("a_colorf_4").get_default_data(), [0.125, 0.25, 0.375, 0.5]
        )
        self.assert_attribute_value(
            compound_node_type.find_input("a_colorf_4_array").get_default_data(),
            [[0.125, 0.25, 0.375, 0.5], [0.5, 0.375, 0.25, 0.125]],
        )
        self.assert_attribute_value(compound_node_type.find_input("a_colorh_3").get_default_data(), [0.5, 0.625, 0.75])
        self.assert_attribute_value(
            compound_node_type.find_input("a_colorh_3_array").get_default_data(),
            [[0.5, 0.625, 0.75], [0.75, 0.625, 0.5]],
        )
        self.assert_attribute_value(
            compound_node_type.find_input("a_colorh_4").get_default_data(), [0.5, 0.625, 0.75, 0.875]
        )
        self.assert_attribute_value(
            compound_node_type.find_input("a_colorh_4_array").get_default_data(),
            [[0.5, 0.625, 0.75, 0.875], [0.875, 0.75, 0.625, 0.5]],
        )
        self.assert_attribute_value(compound_node_type.find_input("a_double").get_default_data(), 4.125)
        self.assert_attribute_value(compound_node_type.find_input("a_double_2").get_default_data(), [4.125, 4.25])
        self.assert_attribute_value(
            compound_node_type.find_input("a_double_2_array").get_default_data(), [[4.125, 4.25], [2.125, 2.25]]
        )
        self.assert_attribute_value(
            compound_node_type.find_input("a_double_3").get_default_data(), [4.125, 4.25, 4.375]
        )
        self.assert_attribute_value(
            compound_node_type.find_input("a_double_3_array").get_default_data(),
            [[4.125, 4.25, 4.375], [2.125, 2.25, 2.375]],
        )
        self.assert_attribute_value(
            compound_node_type.find_input("a_double_4").get_default_data(), [4.125, 4.25, 4.375, 4.5]
        )
        self.assert_attribute_value(
            compound_node_type.find_input("a_double_4_array").get_default_data(),
            [[4.125, 4.25, 4.375, 4.5], [2.125, 2.25, 2.375, 2.5]],
        )
        self.assert_attribute_value(compound_node_type.find_input("a_double_array").get_default_data(), [4.125, 2.125])
        self.assert_attribute_value(compound_node_type.find_input("a_execution").get_default_data(), 0)
        self.assert_attribute_value(compound_node_type.find_input("a_float").get_default_data(), 4.5)
        self.assert_attribute_value(compound_node_type.find_input("a_float_2").get_default_data(), [4.5, 4.625])
        self.assert_attribute_value(
            compound_node_type.find_input("a_float_2_array").get_default_data(), [[4.5, 4.625], [2.5, 2.625]]
        )
        self.assert_attribute_value(compound_node_type.find_input("a_float_3").get_default_data(), [4.5, 4.625, 4.75])
        self.assert_attribute_value(
            compound_node_type.find_input("a_float_3_array").get_default_data(),
            [[4.5, 4.625, 4.75], [2.5, 2.625, 2.75]],
        )
        self.assert_attribute_value(
            compound_node_type.find_input("a_float_4").get_default_data(), [4.5, 4.625, 4.75, 4.875]
        )
        self.assert_attribute_value(
            compound_node_type.find_input("a_float_4_array").get_default_data(),
            [[4.5, 4.625, 4.75, 4.875], [2.5, 2.625, 2.75, 2.875]],
        )
        self.assert_attribute_value(compound_node_type.find_input("a_float_array").get_default_data(), [4.5, 2.5])
        self.assert_attribute_value(
            compound_node_type.find_input("a_frame_4").get_default_data(),
            [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]],
        )
        self.assert_attribute_value(
            compound_node_type.find_input("a_frame_4_array").get_default_data(),
            [
                [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]],
                [[2, 3, 3, 3], [3, 2, 3, 3], [3, 3, 2, 3], [3, 3, 3, 2]],
            ],
        )
        self.assert_attribute_value(compound_node_type.find_input("a_half").get_default_data(), 2.5)
        self.assert_attribute_value(compound_node_type.find_input("a_half_2").get_default_data(), [2.5, 2.625])
        self.assert_attribute_value(
            compound_node_type.find_input("a_half_2_array").get_default_data(), [[2.5, 2.625], [4.5, 4.625]]
        )
        self.assert_attribute_value(compound_node_type.find_input("a_half_3").get_default_data(), [2.5, 2.625, 2.75])
        self.assert_attribute_value(
            compound_node_type.find_input("a_half_3_array").get_default_data(), [[2.5, 2.625, 2.75], [4.5, 4.625, 4.75]]
        )
        self.assert_attribute_value(
            compound_node_type.find_input("a_half_4").get_default_data(), [2.5, 2.625, 2.75, 2.875]
        )
        self.assert_attribute_value(
            compound_node_type.find_input("a_half_4_array").get_default_data(),
            [[2.5, 2.625, 2.75, 2.875], [4.5, 4.625, 4.75, 4.875]],
        )
        self.assert_attribute_value(compound_node_type.find_input("a_half_array").get_default_data(), [2.5, 4.5])
        self.assert_attribute_value(compound_node_type.find_input("a_int").get_default_data(), -32)
        self.assert_attribute_value(compound_node_type.find_input("a_int64").get_default_data(), -46)
        self.assert_attribute_value(compound_node_type.find_input("a_int64_array").get_default_data(), [-46, -64])
        self.assert_attribute_value(compound_node_type.find_input("a_int_2").get_default_data(), [-32, -31])
        self.assert_attribute_value(
            compound_node_type.find_input("a_int_2_array").get_default_data(), [[-32, -31], [-23, -22]]
        )
        self.assert_attribute_value(compound_node_type.find_input("a_int_3").get_default_data(), [-32, -31, -30])
        self.assert_attribute_value(
            compound_node_type.find_input("a_int_3_array").get_default_data(), [[-32, -31, -30], [-23, -22, -21]]
        )
        self.assert_attribute_value(compound_node_type.find_input("a_int_4").get_default_data(), [-32, -31, -30, -29])
        self.assert_attribute_value(
            compound_node_type.find_input("a_int_4_array").get_default_data(),
            [[-32, -31, -30, -29], [-23, -22, -21, -20]],
        )
        self.assert_attribute_value(compound_node_type.find_input("a_int_array").get_default_data(), [-32, -23])
        self.assert_attribute_value(compound_node_type.find_input("a_matrixd_2").get_default_data(), [[1, 0], [0, 1]])
        self.assert_attribute_value(
            compound_node_type.find_input("a_matrixd_2_array").get_default_data(), [[[1, 0], [0, 1]], [[2, 3], [3, 2]]]
        )
        self.assert_attribute_value(
            compound_node_type.find_input("a_matrixd_3").get_default_data(), [[1, 0, 0], [0, 1, 0], [0, 0, 1]]
        )
        self.assert_attribute_value(
            compound_node_type.find_input("a_matrixd_3_array").get_default_data(),
            [[[1, 0, 0], [0, 1, 0], [0, 0, 1]], [[2, 3, 3], [3, 2, 3], [3, 3, 2]]],
        )
        self.assert_attribute_value(
            compound_node_type.find_input("a_matrixd_4").get_default_data(),
            [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]],
        )
        self.assert_attribute_value(
            compound_node_type.find_input("a_matrixd_4_array").get_default_data(),
            [
                [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]],
                [[2, 3, 3, 3], [3, 2, 3, 3], [3, 3, 2, 3], [3, 3, 3, 2]],
            ],
        )
        self.assert_attribute_value(
            compound_node_type.find_input("a_normald_3").get_default_data(), [0.01625, 0.14125, 0.26625]
        )
        self.assert_attribute_value(
            compound_node_type.find_input("a_normald_3_array").get_default_data(),
            [[0.01625, 0.14125, 0.26625], [0.26625, 0.14125, 0.01625]],
        )
        self.assert_attribute_value(
            compound_node_type.find_input("a_normalf_3").get_default_data(), [0.125, 0.25, 0.375]
        )
        self.assert_attribute_value(
            compound_node_type.find_input("a_normalf_3_array").get_default_data(),
            [[0.125, 0.25, 0.375], [0.375, 0.25, 0.125]],
        )
        self.assert_attribute_value(compound_node_type.find_input("a_normalh_3").get_default_data(), [0.5, 0.625, 0.75])
        self.assert_attribute_value(
            compound_node_type.find_input("a_normalh_3_array").get_default_data(),
            [[0.5, 0.625, 0.75], [0.75, 0.625, 0.5]],
        )
        self.assert_attribute_value(compound_node_type.find_input("a_objectId").get_default_data(), 46)
        self.assert_attribute_value(compound_node_type.find_input("a_objectId_array").get_default_data(), [46, 64])
        self.assert_attribute_value(compound_node_type.find_input("a_path").get_default_data(), "/This/Is")
        self.assert_attribute_value(
            compound_node_type.find_input("a_pointd_3").get_default_data(), [0.01625, 0.14125, 0.26625]
        )
        self.assert_attribute_value(
            compound_node_type.find_input("a_pointd_3_array").get_default_data(),
            [[0.01625, 0.14125, 0.26625], [0.26625, 0.14125, 0.01625]],
        )
        self.assert_attribute_value(
            compound_node_type.find_input("a_pointf_3").get_default_data(), [0.125, 0.25, 0.375]
        )
        self.assert_attribute_value(
            compound_node_type.find_input("a_pointf_3_array").get_default_data(),
            [[0.125, 0.25, 0.375], [0.375, 0.25, 0.125]],
        )
        self.assert_attribute_value(compound_node_type.find_input("a_pointh_3").get_default_data(), [0.5, 0.625, 0.75])
        self.assert_attribute_value(
            compound_node_type.find_input("a_pointh_3_array").get_default_data(),
            [[0.5, 0.625, 0.75], [0.75, 0.625, 0.5]],
        )
        self.assert_attribute_value(
            compound_node_type.find_input("a_quatd_4").get_default_data(), [0.01625, 0.14125, 0.26625, 0.78]
        )
        self.assert_attribute_value(
            compound_node_type.find_input("a_quatd_4_array").get_default_data(),
            [[0.01625, 0.14125, 0.26625, 0.78], [0.14125, 0.26625, 0.39125, 0.51625]],
        )
        self.assert_attribute_value(
            compound_node_type.find_input("a_quatf_4").get_default_data(), [0.125, 0.25, 0.375, 0.5]
        )
        self.assert_attribute_value(
            compound_node_type.find_input("a_quatf_4_array").get_default_data(),
            [[0.125, 0.25, 0.375, 0.5], [0.25, 0.375, 0.5, 0.625]],
        )
        self.assert_attribute_value(compound_node_type.find_input("a_quath_4").get_default_data(), [0, 0.25, 0.5, 0.75])
        self.assert_attribute_value(
            compound_node_type.find_input("a_quath_4_array").get_default_data(),
            [[0, 0.25, 0.5, 0.75], [0.125, 0.375, 0.625, 0.875]],
        )
        self.assert_attribute_value(compound_node_type.find_input("a_string").get_default_data(), "Anakin")
        self.assert_attribute_value(
            compound_node_type.find_input("a_texcoordd_2").get_default_data(), [0.01625, 0.14125]
        )
        self.assert_attribute_value(
            compound_node_type.find_input("a_texcoordd_2_array").get_default_data(),
            [[0.01625, 0.14125], [0.14125, 0.01625]],
        )
        self.assert_attribute_value(
            compound_node_type.find_input("a_texcoordd_3").get_default_data(), [0.01625, 0.14125, 0.26625]
        )
        self.assert_attribute_value(
            compound_node_type.find_input("a_texcoordd_3_array").get_default_data(),
            [[0.01625, 0.14125, 0.26625], [0.26625, 0.14125, 0.01625]],
        )
        self.assert_attribute_value(compound_node_type.find_input("a_texcoordf_2").get_default_data(), [0.125, 0.25])
        self.assert_attribute_value(
            compound_node_type.find_input("a_texcoordf_2_array").get_default_data(), [[0.125, 0.25], [0.25, 0.125]]
        )
        self.assert_attribute_value(
            compound_node_type.find_input("a_texcoordf_3").get_default_data(), [0.125, 0.25, 0.375]
        )
        self.assert_attribute_value(
            compound_node_type.find_input("a_texcoordf_3_array").get_default_data(),
            [[0.125, 0.25, 0.375], [0.375, 0.25, 0.125]],
        )
        self.assert_attribute_value(compound_node_type.find_input("a_texcoordh_2").get_default_data(), [0.5, 0.625])
        self.assert_attribute_value(
            compound_node_type.find_input("a_texcoordh_2_array").get_default_data(), [[0.5, 0.625], [0.625, 0.5]]
        )
        self.assert_attribute_value(
            compound_node_type.find_input("a_texcoordh_3").get_default_data(), [0.5, 0.625, 0.75]
        )
        self.assert_attribute_value(
            compound_node_type.find_input("a_texcoordh_3_array").get_default_data(),
            [[0.5, 0.625, 0.75], [0.75, 0.625, 0.5]],
        )
        self.assert_attribute_value(compound_node_type.find_input("a_timecode").get_default_data(), 5)
        self.assert_attribute_value(compound_node_type.find_input("a_timecode_array").get_default_data(), [5, 6])
        self.assert_attribute_value(compound_node_type.find_input("a_token").get_default_data(), "Ahsoka")
        self.assert_attribute_value(
            compound_node_type.find_input("a_token_array").get_default_data(), ["Ahsoka", "Tano"]
        )
        self.assert_attribute_value(compound_node_type.find_input("a_uchar").get_default_data(), 12)
        self.assert_attribute_value(compound_node_type.find_input("a_uchar_array").get_default_data(), [12, 8])
        self.assert_attribute_value(compound_node_type.find_input("a_uint").get_default_data(), 32)
        self.assert_attribute_value(compound_node_type.find_input("a_uint64").get_default_data(), 46)
        self.assert_attribute_value(compound_node_type.find_input("a_uint64_array").get_default_data(), [46, 64])
        self.assert_attribute_value(compound_node_type.find_input("a_uint_array").get_default_data(), [32, 23])
        self.assert_attribute_value(
            compound_node_type.find_input("a_vectord_3").get_default_data(), [0.01625, 0.14125, 0.26625]
        )
        self.assert_attribute_value(
            compound_node_type.find_input("a_vectord_3_array").get_default_data(),
            [[0.01625, 0.14125, 0.26625], [0.26625, 0.14125, 0.01625]],
        )
        self.assert_attribute_value(
            compound_node_type.find_input("a_vectorf_3").get_default_data(), [0.125, 0.25, 0.375]
        )
        self.assert_attribute_value(
            compound_node_type.find_input("a_vectorf_3_array").get_default_data(),
            [[0.125, 0.25, 0.375], [0.375, 0.25, 0.125]],
        )
        self.assert_attribute_value(compound_node_type.find_input("a_vectorh_3").get_default_data(), [0.5, 0.625, 0.75])
        self.assert_attribute_value(
            compound_node_type.find_input("a_vectorh_3_array").get_default_data(),
            [[0.5, 0.625, 0.75], [0.75, 0.625, 0.5]],
        )

        # create a graph that uses the compound graph
        keys = og.Controller.Keys
        controller = og.Controller()
        (_, nodes, _, _) = controller.edit(
            self._test_graph_path,
            {
                keys.CREATE_NODES: [
                    ("Compound", f"{self.get_compound_folder()}/Compound"),
                ]
            },
        )
        node = nodes[0]

        # validate that the generated nodes have the proper default set as well.
        self.assert_attribute_value(node.get_attribute("inputs:a_bool").get(), 0)
        self.assert_attribute_value(node.get_attribute("inputs:a_bool_array").get(), [0, 1])
        self.assert_attribute_value(node.get_attribute("inputs:a_colord_3").get(), [0.01625, 0.14125, 0.26625])
        self.assert_attribute_value(
            node.get_attribute("inputs:a_colord_3_array").get(),
            [[0.01625, 0.14125, 0.26625], [0.26625, 0.14125, 0.01625]],
        )
        self.assert_attribute_value(node.get_attribute("inputs:a_colord_4").get(), [0.01625, 0.14125, 0.26625, 0.39125])
        self.assert_attribute_value(
            node.get_attribute("inputs:a_colord_4_array").get(),
            [[0.01625, 0.14125, 0.26625, 0.39125], [0.39125, 0.26625, 0.14125, 0.01625]],
        )
        self.assert_attribute_value(node.get_attribute("inputs:a_colorf_3").get(), [0.125, 0.25, 0.375])
        self.assert_attribute_value(
            node.get_attribute("inputs:a_colorf_3_array").get(), [[0.125, 0.25, 0.375], [0.375, 0.25, 0.125]]
        )
        self.assert_attribute_value(node.get_attribute("inputs:a_colorf_4").get(), [0.125, 0.25, 0.375, 0.5])
        self.assert_attribute_value(
            node.get_attribute("inputs:a_colorf_4_array").get(), [[0.125, 0.25, 0.375, 0.5], [0.5, 0.375, 0.25, 0.125]]
        )
        self.assert_attribute_value(node.get_attribute("inputs:a_colorh_3").get(), [0.5, 0.625, 0.75])
        self.assert_attribute_value(
            node.get_attribute("inputs:a_colorh_3_array").get(), [[0.5, 0.625, 0.75], [0.75, 0.625, 0.5]]
        )
        self.assert_attribute_value(node.get_attribute("inputs:a_colorh_4").get(), [0.5, 0.625, 0.75, 0.875])
        self.assert_attribute_value(
            node.get_attribute("inputs:a_colorh_4_array").get(), [[0.5, 0.625, 0.75, 0.875], [0.875, 0.75, 0.625, 0.5]]
        )
        self.assert_attribute_value(node.get_attribute("inputs:a_double").get(), 4.125)
        self.assert_attribute_value(node.get_attribute("inputs:a_double_2").get(), [4.125, 4.25])
        self.assert_attribute_value(node.get_attribute("inputs:a_double_2_array").get(), [[4.125, 4.25], [2.125, 2.25]])
        self.assert_attribute_value(node.get_attribute("inputs:a_double_3").get(), [4.125, 4.25, 4.375])
        self.assert_attribute_value(
            node.get_attribute("inputs:a_double_3_array").get(), [[4.125, 4.25, 4.375], [2.125, 2.25, 2.375]]
        )
        self.assert_attribute_value(node.get_attribute("inputs:a_double_4").get(), [4.125, 4.25, 4.375, 4.5])
        self.assert_attribute_value(
            node.get_attribute("inputs:a_double_4_array").get(), [[4.125, 4.25, 4.375, 4.5], [2.125, 2.25, 2.375, 2.5]]
        )
        self.assert_attribute_value(node.get_attribute("inputs:a_double_array").get(), [4.125, 2.125])
        self.assert_attribute_value(node.get_attribute("inputs:a_execution").get(), 0)
        self.assert_attribute_value(node.get_attribute("inputs:a_float").get(), 4.5)
        self.assert_attribute_value(node.get_attribute("inputs:a_float_2").get(), [4.5, 4.625])
        self.assert_attribute_value(node.get_attribute("inputs:a_float_2_array").get(), [[4.5, 4.625], [2.5, 2.625]])
        self.assert_attribute_value(node.get_attribute("inputs:a_float_3").get(), [4.5, 4.625, 4.75])
        self.assert_attribute_value(
            node.get_attribute("inputs:a_float_3_array").get(), [[4.5, 4.625, 4.75], [2.5, 2.625, 2.75]]
        )
        self.assert_attribute_value(node.get_attribute("inputs:a_float_4").get(), [4.5, 4.625, 4.75, 4.875])
        self.assert_attribute_value(
            node.get_attribute("inputs:a_float_4_array").get(), [[4.5, 4.625, 4.75, 4.875], [2.5, 2.625, 2.75, 2.875]]
        )
        self.assert_attribute_value(node.get_attribute("inputs:a_float_array").get(), [4.5, 2.5])
        self.assert_attribute_value(
            node.get_attribute("inputs:a_frame_4").get(), [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]
        )
        self.assert_attribute_value(
            node.get_attribute("inputs:a_frame_4_array").get(),
            [
                [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]],
                [[2, 3, 3, 3], [3, 2, 3, 3], [3, 3, 2, 3], [3, 3, 3, 2]],
            ],
        )
        self.assert_attribute_value(node.get_attribute("inputs:a_half").get(), 2.5)
        self.assert_attribute_value(node.get_attribute("inputs:a_half_2").get(), [2.5, 2.625])
        self.assert_attribute_value(node.get_attribute("inputs:a_half_2_array").get(), [[2.5, 2.625], [4.5, 4.625]])
        self.assert_attribute_value(node.get_attribute("inputs:a_half_3").get(), [2.5, 2.625, 2.75])
        self.assert_attribute_value(
            node.get_attribute("inputs:a_half_3_array").get(), [[2.5, 2.625, 2.75], [4.5, 4.625, 4.75]]
        )
        self.assert_attribute_value(node.get_attribute("inputs:a_half_4").get(), [2.5, 2.625, 2.75, 2.875])
        self.assert_attribute_value(
            node.get_attribute("inputs:a_half_4_array").get(), [[2.5, 2.625, 2.75, 2.875], [4.5, 4.625, 4.75, 4.875]]
        )
        self.assert_attribute_value(node.get_attribute("inputs:a_half_array").get(), [2.5, 4.5])
        self.assert_attribute_value(node.get_attribute("inputs:a_int").get(), -32)
        self.assert_attribute_value(node.get_attribute("inputs:a_int64").get(), -46)
        self.assert_attribute_value(node.get_attribute("inputs:a_int64_array").get(), [-46, -64])
        self.assert_attribute_value(node.get_attribute("inputs:a_int_2").get(), [-32, -31])
        self.assert_attribute_value(node.get_attribute("inputs:a_int_2_array").get(), [[-32, -31], [-23, -22]])
        self.assert_attribute_value(node.get_attribute("inputs:a_int_3").get(), [-32, -31, -30])
        self.assert_attribute_value(
            node.get_attribute("inputs:a_int_3_array").get(), [[-32, -31, -30], [-23, -22, -21]]
        )
        self.assert_attribute_value(node.get_attribute("inputs:a_int_4").get(), [-32, -31, -30, -29])
        self.assert_attribute_value(
            node.get_attribute("inputs:a_int_4_array").get(), [[-32, -31, -30, -29], [-23, -22, -21, -20]]
        )
        self.assert_attribute_value(node.get_attribute("inputs:a_int_array").get(), [-32, -23])
        self.assert_attribute_value(node.get_attribute("inputs:a_matrixd_2").get(), [[1, 0], [0, 1]])
        self.assert_attribute_value(
            node.get_attribute("inputs:a_matrixd_2_array").get(), [[[1, 0], [0, 1]], [[2, 3], [3, 2]]]
        )
        self.assert_attribute_value(node.get_attribute("inputs:a_matrixd_3").get(), [[1, 0, 0], [0, 1, 0], [0, 0, 1]])
        self.assert_attribute_value(
            node.get_attribute("inputs:a_matrixd_3_array").get(),
            [[[1, 0, 0], [0, 1, 0], [0, 0, 1]], [[2, 3, 3], [3, 2, 3], [3, 3, 2]]],
        )
        self.assert_attribute_value(
            node.get_attribute("inputs:a_matrixd_4").get(), [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]
        )
        self.assert_attribute_value(
            node.get_attribute("inputs:a_matrixd_4_array").get(),
            [
                [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]],
                [[2, 3, 3, 3], [3, 2, 3, 3], [3, 3, 2, 3], [3, 3, 3, 2]],
            ],
        )
        self.assert_attribute_value(node.get_attribute("inputs:a_normald_3").get(), [0.01625, 0.14125, 0.26625])
        self.assert_attribute_value(
            node.get_attribute("inputs:a_normald_3_array").get(),
            [[0.01625, 0.14125, 0.26625], [0.26625, 0.14125, 0.01625]],
        )
        self.assert_attribute_value(node.get_attribute("inputs:a_normalf_3").get(), [0.125, 0.25, 0.375])
        self.assert_attribute_value(
            node.get_attribute("inputs:a_normalf_3_array").get(), [[0.125, 0.25, 0.375], [0.375, 0.25, 0.125]]
        )
        self.assert_attribute_value(node.get_attribute("inputs:a_normalh_3").get(), [0.5, 0.625, 0.75])
        self.assert_attribute_value(
            node.get_attribute("inputs:a_normalh_3_array").get(), [[0.5, 0.625, 0.75], [0.75, 0.625, 0.5]]
        )
        self.assert_attribute_value(node.get_attribute("inputs:a_objectId").get(), 46)
        self.assert_attribute_value(node.get_attribute("inputs:a_objectId_array").get(), [46, 64])
        self.assert_attribute_value(node.get_attribute("inputs:a_path").get(), "/This/Is")
        self.assert_attribute_value(node.get_attribute("inputs:a_pointd_3").get(), [0.01625, 0.14125, 0.26625])
        self.assert_attribute_value(
            node.get_attribute("inputs:a_pointd_3_array").get(),
            [[0.01625, 0.14125, 0.26625], [0.26625, 0.14125, 0.01625]],
        )
        self.assert_attribute_value(node.get_attribute("inputs:a_pointf_3").get(), [0.125, 0.25, 0.375])
        self.assert_attribute_value(
            node.get_attribute("inputs:a_pointf_3_array").get(), [[0.125, 0.25, 0.375], [0.375, 0.25, 0.125]]
        )
        self.assert_attribute_value(node.get_attribute("inputs:a_pointh_3").get(), [0.5, 0.625, 0.75])
        self.assert_attribute_value(
            node.get_attribute("inputs:a_pointh_3_array").get(), [[0.5, 0.625, 0.75], [0.75, 0.625, 0.5]]
        )
        self.assert_attribute_value(node.get_attribute("inputs:a_quatd_4").get(), [0.01625, 0.14125, 0.26625, 0.78])
        self.assert_attribute_value(
            node.get_attribute("inputs:a_quatd_4_array").get(),
            [[0.01625, 0.14125, 0.26625, 0.78], [0.14125, 0.26625, 0.39125, 0.51625]],
        )
        self.assert_attribute_value(node.get_attribute("inputs:a_quatf_4").get(), [0.125, 0.25, 0.375, 0.5])
        self.assert_attribute_value(
            node.get_attribute("inputs:a_quatf_4_array").get(), [[0.125, 0.25, 0.375, 0.5], [0.25, 0.375, 0.5, 0.625]]
        )
        self.assert_attribute_value(node.get_attribute("inputs:a_quath_4").get(), [0, 0.25, 0.5, 0.75])
        self.assert_attribute_value(
            node.get_attribute("inputs:a_quath_4_array").get(), [[0, 0.25, 0.5, 0.75], [0.125, 0.375, 0.625, 0.875]]
        )
        self.assert_attribute_value(node.get_attribute("inputs:a_string").get(), "Anakin")
        self.assert_attribute_value(node.get_attribute("inputs:a_texcoordd_2").get(), [0.01625, 0.14125])
        self.assert_attribute_value(
            node.get_attribute("inputs:a_texcoordd_2_array").get(), [[0.01625, 0.14125], [0.14125, 0.01625]]
        )
        self.assert_attribute_value(node.get_attribute("inputs:a_texcoordd_3").get(), [0.01625, 0.14125, 0.26625])
        self.assert_attribute_value(
            node.get_attribute("inputs:a_texcoordd_3_array").get(),
            [[0.01625, 0.14125, 0.26625], [0.26625, 0.14125, 0.01625]],
        )
        self.assert_attribute_value(node.get_attribute("inputs:a_texcoordf_2").get(), [0.125, 0.25])
        self.assert_attribute_value(
            node.get_attribute("inputs:a_texcoordf_2_array").get(), [[0.125, 0.25], [0.25, 0.125]]
        )
        self.assert_attribute_value(node.get_attribute("inputs:a_texcoordf_3").get(), [0.125, 0.25, 0.375])
        self.assert_attribute_value(
            node.get_attribute("inputs:a_texcoordf_3_array").get(), [[0.125, 0.25, 0.375], [0.375, 0.25, 0.125]]
        )
        self.assert_attribute_value(node.get_attribute("inputs:a_texcoordh_2").get(), [0.5, 0.625])
        self.assert_attribute_value(
            node.get_attribute("inputs:a_texcoordh_2_array").get(), [[0.5, 0.625], [0.625, 0.5]]
        )
        self.assert_attribute_value(node.get_attribute("inputs:a_texcoordh_3").get(), [0.5, 0.625, 0.75])
        self.assert_attribute_value(
            node.get_attribute("inputs:a_texcoordh_3_array").get(), [[0.5, 0.625, 0.75], [0.75, 0.625, 0.5]]
        )
        self.assert_attribute_value(node.get_attribute("inputs:a_timecode").get(), 5)
        self.assert_attribute_value(node.get_attribute("inputs:a_timecode_array").get(), [5, 6])
        self.assert_attribute_value(node.get_attribute("inputs:a_token").get(), "Ahsoka")
        self.assert_attribute_value(node.get_attribute("inputs:a_token_array").get(), ["Ahsoka", "Tano"])
        self.assert_attribute_value(node.get_attribute("inputs:a_uchar").get(), 12)
        self.assert_attribute_value(node.get_attribute("inputs:a_uchar_array").get(), [12, 8])
        self.assert_attribute_value(node.get_attribute("inputs:a_uint").get(), 32)
        self.assert_attribute_value(node.get_attribute("inputs:a_uint64").get(), 46)
        self.assert_attribute_value(node.get_attribute("inputs:a_uint64_array").get(), [46, 64])
        self.assert_attribute_value(node.get_attribute("inputs:a_uint_array").get(), [32, 23])
        self.assert_attribute_value(node.get_attribute("inputs:a_vectord_3").get(), [0.01625, 0.14125, 0.26625])
        self.assert_attribute_value(
            node.get_attribute("inputs:a_vectord_3_array").get(),
            [[0.01625, 0.14125, 0.26625], [0.26625, 0.14125, 0.01625]],
        )
        self.assert_attribute_value(node.get_attribute("inputs:a_vectorf_3").get(), [0.125, 0.25, 0.375])
        self.assert_attribute_value(
            node.get_attribute("inputs:a_vectorf_3_array").get(), [[0.125, 0.25, 0.375], [0.375, 0.25, 0.125]]
        )
        self.assert_attribute_value(node.get_attribute("inputs:a_vectorh_3").get(), [0.5, 0.625, 0.75])
        self.assert_attribute_value(
            node.get_attribute("inputs:a_vectorh_3_array").get(), [[0.5, 0.625, 0.75], [0.75, 0.625, 0.5]]
        )

    def _create_test_action_compound(
        self, compound_name, graph_name="Graph", namespace="local.nodes.", compound_dir=None
    ):
        """Creates a compound node/graph that wraps an add node"""
        # create a compound node
        (success, schema_prim) = ogu.cmds.CreateCompoundNodeType(
            compound_name=compound_name,
            graph_name=graph_name,
            namespace=namespace,
            folder=compound_dir,
            evaluator_type="execution",
        )
        if not success:
            raise og.OmniGraphError("CreateNodeType failed")

        compound_path = str(schema_prim.GetPrim().GetPath())
        graph_path = f"{compound_path}/{graph_name}"
        node_type = get_node_type_from_schema_prim(schema_prim)

        keys = og.Controller.Keys
        controller = og.Controller()
        (_, _, _, _) = controller.edit(
            graph_path,
            {
                keys.CREATE_NODES: [
                    ("OnTick", "omni.graph.action.OnTick"),
                    ("Branch", "omni.graph.action.Branch"),
                    ("WriteTrue", "omni.graph.nodes.WritePrimAttribute"),
                    ("WriteFalse", "omni.graph.nodes.WritePrimAttribute"),
                ],
                keys.CONNECT: [
                    ("OnTick.outputs:tick", "Branch.inputs:execIn"),
                    ("Branch.outputs:execTrue", "WriteTrue.inputs:execIn"),
                    ("Branch.outputs:execFalse", "WriteFalse.inputs:execIn"),
                ],
                keys.CREATE_PRIMS: [("/World/Prim", {"val": ("double", 0.0)})],
                keys.SET_VALUES: [
                    ("OnTick.inputs:onlyPlayback", False),
                    ("WriteTrue.inputs:name", "val"),
                    ("WriteTrue.inputs:primPath", "/World/Prim"),
                    ("WriteTrue.inputs:usePath", True),
                    ("WriteFalse.inputs:name", "val"),
                    ("WriteFalse.inputs:primPath", "/World/Prim"),
                    ("WriteFalse.inputs:usePath", True),
                ],
            },
        )

        # assign inputs and outputs
        ogu.cmds.CreateCompoundNodeTypeInput(node_type=node_type, input_name="execIn", default_type="execution")
        ogu.cmds.CreateCompoundNodeTypeInput(
            node_type=node_type,
            input_name="valTrue",
            attribute_path=Sdf.Path(f"{graph_path}/WriteTrue.inputs:value"),
            default_type="double",
        )
        ogu.cmds.CreateCompoundNodeTypeInput(
            node_type=node_type,
            input_name="valFalse",
            attribute_path=Sdf.Path(f"{graph_path}/WriteFalse.inputs:value"),
            default_type="double",
        )
        ogu.cmds.CreateCompoundNodeTypeInput(
            node_type=node_type,
            input_name="condition",
            attribute_path=Sdf.Path(f"{graph_path}/Branch.inputs:condition"),
            default_type="bool",
        )
        ogu.cmds.CreateCompoundNodeTypeOutput(
            node_type=node_type,
            output_name="execOut",
            attribute_path=Sdf.Path(f"{graph_path}/WriteTrue.outputs:execOut"),
            default_type="execution",
        )
        return (compound_path, graph_path)

    async def test_action_compounds(self):
        """
        Test Action Graph compound nodes
        """
        compound_path, _ = self._create_test_action_compound("ExecCompound")
        self._create_test_compound("PushCompound")
        stage = omni.usd.get_context().get_stage()
        push_compound_path = f"{self.get_compound_folder()}/PushCompound"

        # create a graph that uses the compound graph
        keys = og.Controller.Keys
        controller = og.Controller()
        (graph, (exec_node, push_node, _, _, _, _), _, _) = controller.edit(
            {"graph_path": self._test_graph_path, "evaluator_name": "execution"},
            {
                keys.CREATE_NODES: [
                    ("ExecCompound", compound_path),
                    ("PushCompound", push_compound_path),
                    ("OnTick", "omni.graph.action.OnTick"),
                    ("Constant1", "omni.graph.nodes.ConstantDouble"),
                    ("Constant2", "omni.graph.nodes.ConstantDouble"),
                    ("ConstantBool", "omni.graph.nodes.ConstantBool"),
                ],
                keys.CONNECT: [
                    ("OnTick.outputs:tick", "ExecCompound.inputs:execIn"),
                    ("Constant1.inputs:value", "PushCompound.inputs:input_a"),
                    ("Constant2.inputs:value", "PushCompound.inputs:input_b"),
                    ("PushCompound.outputs:value", "ExecCompound.inputs:valTrue"),
                    ("Constant2.inputs:value", "ExecCompound.inputs:valFalse"),
                    ("ConstantBool.inputs:value", "ExecCompound.inputs:condition"),
                ],
                keys.SET_VALUES: [
                    ("Constant1.inputs:value", 1.0),
                    ("Constant2.inputs:value", 2.0),
                    ("ConstantBool.inputs:value", False),
                    ("OnTick.inputs:onlyPlayback", False),
                ],
            },
        )
        await og.Controller.evaluate(graph)

        self.assertTrue(bool(exec_node.get_compound_graph_instance()))
        self.assertEqual(len(exec_node.get_compound_graph_instance().get_nodes()), 4)
        self.assertTrue(bool(push_node.get_compound_graph_instance()))

        stage = omni.usd.get_context().get_stage()
        self.assertEqual(stage.GetAttributeAtPath("/World/Prim.val").Get(), 2.0)

        # Set the Branch to go to WriteTrue, which will take the value from PushCompound.outputs:value
        # which should be 1.0 + 2.0
        controller.edit(self._test_graph_path, {keys.SET_VALUES: ("ConstantBool.inputs:value", True)})
        await og.Controller.evaluate(graph)

        self.assertEqual(stage.GetAttributeAtPath("/World/Prim.val").Get(), 3.0)

    # -------------------------------------------------------------------------
    async def test_compound_node_upgrade_to_schema_api(self):
        """
        Tests that loading a file with a compound node generated prior to the introduction of
        compoundNodeAPI gets properly migrated.
        """
        # The preschema file contains a graph with a simple compound node
        # [Constant]->| Compound Node |->[Absolute]
        # [Constant]->|               |
        #
        # With a compound defined as
        # [Add]->[Absolute]
        # The constants are set to 1 and 2, so the result trivially evaluates to 3
        (result, error) = await ogts.load_test_file(self._test_preschema_file, use_caller_subdirectory=True)
        self.assertTrue(result, error)

        stage = omni.usd.get_context().get_stage()

        graph_prim_path = "/World/PushGraph"
        compound_path = f"{graph_prim_path}/Compound"
        result_path = f"{graph_prim_path}/absolute_01.outputs:absolute"

        # validate the graph evaluates as expeected
        graph = og.Controller.graph(graph_prim_path)
        await og.Controller.evaluate(graph)

        self.assertTrue(graph.is_valid())
        attr = og.Controller.attribute(result_path)
        self.assertTrue(attr.is_valid())
        self.assertEqual(3, og.Controller.get(attr))

        # validate the compound node USD is upgraded as expected
        compound_node_prim = stage.GetPrimAtPath(compound_path)
        self.assertTrue(compound_node_prim.IsValid())
        # the old relationship has been removed, and replaced with the schemaAPI
        self.assertFalse(compound_node_prim.GetRelationship("omni:graph:compound").IsValid())
        self.assertTrue(compound_node_prim.HasAPI(OmniGraphSchema.CompoundNodeAPI))
        schema_api = OmniGraphSchema.CompoundNodeAPI(compound_node_prim)
        self.assertGreater(len(schema_api.GetCompoundGraphRel().GetTargets()), 0)
        self.assertEqual(schema_api.GetCompoundTypeAttr().Get(), "nodetype")

    # -------------------------------------------------------------------------
    async def test_enter_compound(self):
        """
        Test that querying a node in a compound subgraph will pass validation. This exercises a code-path that is used
        by kit-graphs when you double-click and enter a compound. The graph itself remains the top-level graph (as this
        is set in the graph model), but we are able to step in and look at nodes in a child graph
        """
        self._create_test_compound("Compound")
        self._create_and_test_graph_from_add_compound(f"{self.get_compound_folder()}/Compound")

        # Verify that we do not get an error in ObjectLookup __verify_graph
        node = og.Controller.node(
            f"{self._test_graph_path}/Compound/Graph/Add_Node", og.get_graph_by_path(self._test_graph_path)
        )
        self.assertTrue(node.is_valid())

    # -------------------------------------------------------------------------
    async def test_get_owning_compound_node_api(self):
        """
        Tests the og.graph.get_owning_compound_node_api available on og.Graph.
        """
        controller = og.Controller(update_usd=True)
        keys = controller.Keys

        # Simple graph with nested compounds
        (graph, (compound_1, compound_2), _, node_map) = controller.edit(
            self._test_graph_path,
            {
                keys.CREATE_NODES: [
                    (
                        "Compound1",
                        {
                            keys.CREATE_NODES: [
                                ("Add1", "omni.graph.nodes.Add"),
                                (
                                    "Compound3",
                                    {
                                        keys.CREATE_NODES: [
                                            ("Add3", "omni.graph.nodes.Add"),
                                        ]
                                    },
                                ),
                            ],
                        },
                    ),
                    (
                        "Compound2",
                        {
                            keys.CREATE_NODES: [
                                ("Add2", "omni.graph.nodes.Add"),
                            ]
                        },
                    ),
                ],
            },
        )

        self.assertTrue(graph.is_valid())
        self.assertFalse(graph.get_owning_compound_node().is_valid())

        compound_graph_1 = compound_1.get_compound_graph_instance()
        compound_graph_2 = compound_2.get_compound_graph_instance()
        self.assertTrue(compound_graph_1.is_valid())
        self.assertTrue(compound_graph_2.is_valid())

        compound_3 = node_map["Compound3"]
        self.assertTrue(compound_3.is_valid())
        compound_graph_3 = compound_3.get_compound_graph_instance()
        self.assertTrue(compound_graph_3.is_valid())

        self.assertEqual(compound_graph_1.get_owning_compound_node(), compound_1)
        self.assertEqual(compound_graph_2.get_owning_compound_node(), compound_2)
        self.assertEqual(compound_graph_3.get_owning_compound_node(), compound_3)
        self.assertEqual(compound_3.get_graph().get_owning_compound_node(), compound_1)
        self.assertFalse(compound_1.get_graph().get_owning_compound_node().is_valid())

        add1 = node_map["Add1"]
        self.assertEquals(add1.get_graph().get_owning_compound_node(), compound_1)
        add2 = node_map["Add2"]
        self.assertEquals(add2.get_graph().get_owning_compound_node(), compound_2)
        add3 = node_map["Add3"]
        self.assertEquals(add3.get_graph().get_owning_compound_node(), compound_3)
