import pytest from workflows.errors import CyclicDependencyError, UnknownVariableError, WorkflowError from workflows.utils import _create_variable_step_mapping, create_dependency_graph, topological_sort # Dummy classes to simulate Workflow, Step, and Field class DummyField: def __init__(self, name, type="str", variable=None): self.name = name self.type = type # For input fields, variable property is needed self.variable = variable if variable is not None else name class DummyStep: def __init__(self, input_fields, output_fields): self.input_fields = input_fields self.output_fields = output_fields class DummyWorkflow: def __init__(self, steps): # steps is a dict with key as step_id and value as DummyStep self.steps = steps # Tests for _create_variable_step_mapping def test_create_variable_step_mapping_success(): # Create a workflow with two steps producing unique output variables step_a = DummyStep(input_fields=[], output_fields=[DummyField("out1")]) step_b = DummyStep(input_fields=[], output_fields=[DummyField("out2")]) workflow = DummyWorkflow({"A": step_a, "B": step_b}) mapping = _create_variable_step_mapping(workflow) assert mapping == {"A.out1": "A", "B.out2": "B"} def test_create_variable_step_mapping_duplicate(): # Create a workflow where two steps produce an output with same name step_a = DummyStep(input_fields=[], output_fields=[DummyField("out"), DummyField("out")]) workflow = DummyWorkflow({"A": step_a}) with pytest.raises(WorkflowError): _create_variable_step_mapping(workflow) def test_create_variable_step_mapping_empty(): """Test _create_variable_step_mapping with an empty workflow should return an empty mapping.""" workflow = DummyWorkflow({}) mapping = _create_variable_step_mapping(workflow) assert mapping == {} def test_create_variable_step_mapping_multiple_outputs(): """Test a workflow where a single step produces multiple outputs with unique names.""" step = DummyStep(input_fields=[], output_fields=[DummyField("out1"), DummyField("out2")]) workflow = DummyWorkflow({"A": step}) mapping = _create_variable_step_mapping(workflow) assert mapping == {"A.out1": "A", "A.out2": "A"} # Tests for create_dependency_graph def test_create_dependency_graph_success_with_dependency(): # Step A produces 'A.out', which is used as input in step B step_a = DummyStep(input_fields=[], output_fields=[DummyField("out")]) # For input_fields, explicitly set variable to reference A.out step_b = DummyStep(input_fields=[DummyField("dummy", variable="A.out")], output_fields=[DummyField("result")]) workflow = DummyWorkflow({"A": step_a, "B": step_b}) # No external input provided for A.out so dependency must be created deps = create_dependency_graph(workflow, input_values={}) # Step B depends on step A assert deps["B"] == {"A"} # Step A has no dependencies assert deps["A"] == set() def test_create_dependency_graph_success_with_external_input(): # Step B expects an input, but it is provided externally step_b = DummyStep( input_fields=[DummyField("param", variable="external_param")], output_fields=[DummyField("result")] ) workflow = DummyWorkflow({"B": step_b}) # Provide external input for external_param deps = create_dependency_graph(workflow, input_values={"external_param": 42}) # With external input, no dependency is needed assert deps["B"] == set() def test_create_dependency_graph_unknown_variable(): # Step B expects an input that is neither produced by any step nor provided externally step_b = DummyStep( input_fields=[DummyField("param", variable="non_existent")], output_fields=[DummyField("result")] ) workflow = DummyWorkflow({"B": step_b}) with pytest.raises(UnknownVariableError): _ = create_dependency_graph(workflow, input_values={}) def test_create_dependency_graph_complex(): """Test create_dependency_graph on a more complex workflow with multiple dependencies.""" # Step A produces A.out, Step B uses A.out, Step C uses B.out, and Step D uses both A.out and B.out step_a = DummyStep(input_fields=[], output_fields=[DummyField("out")]) step_b = DummyStep(input_fields=[DummyField("inp", variable="A.out")], output_fields=[DummyField("out")]) step_c = DummyStep(input_fields=[DummyField("inp", variable="B.out")], output_fields=[DummyField("result")]) step_d = DummyStep( input_fields=[DummyField("inp1", variable="A.out"), DummyField("inp2", variable="B.out")], output_fields=[DummyField("final")], ) workflow = DummyWorkflow({"A": step_a, "B": step_b, "C": step_c, "D": step_d}) # Provide external input for "B.out" so that step B's output isn't expected to come from a step # However, to simulate dependency, assume external input is not provided for the dependencies used in step C and D # Therefore, workflow must resolve A.out for step B, and then step B produces B.out for steps C and D. # Let's not provide any external input, so both dependencies are created. deps = create_dependency_graph(workflow, input_values={}) # Expected dependencies: # B depends on A # C depends on B # D depends on both A and B assert deps["B"] == {"A"} assert deps["C"] == {"B"} assert deps["D"] == {"A", "B"} # Tests for topological_sort def test_topological_sort_success(): # Create a simple dependency graph: A -> B -> C deps = {"A": set(), "B": {"A"}, "C": {"B"}} order = topological_sort(deps) # Check that order satisfies dependencies: A before B, B before C assert order.index("A") < order.index("B") < order.index("C") def test_topological_sort_cycle(): # Create a cyclic dependency: A -> B and B -> A deps = {"A": {"B"}, "B": {"A"}} with pytest.raises(CyclicDependencyError): _ = topological_sort(deps) def test_topological_sort_single_node(): """Test topological_sort on a graph with a single node and no dependencies.""" deps = {"A": set()} order = topological_sort(deps) assert order == ["A"] def test_topological_sort_disconnected(): """Test topological_sort on a graph with disconnected nodes (no dependencies among them).""" deps = {"A": set(), "B": set(), "C": set()} order = topological_sort(deps) # The order can be in any permutation, but must contain all nodes assert set(order) == {"A", "B", "C"}