def setUp(self): workflow = Workflow(working_dir='/some/dir') self.target1 = workflow.target('TestTarget1', inputs=[], outputs=['test_output1.txt']) self.target2 = workflow.target('TestTarget2', inputs=['test_output1.txt'], outputs=['test_output2.txt']) self.target3 = workflow.target('TestTarget3', inputs=['test_output1.txt'], outputs=['test_output3.txt']) self.target4 = workflow.target('TestTarget4', inputs=['test_output2.txt', 'test_output3.txt'], outputs=['final_output.txt']) self.graph = Graph(targets=workflow.targets)
def test_two_targets_producing_the_same_file_but_declared_with_rel_and_abs_path( mock_os_path_exists ): workflow = Workflow(working_dir="/some/dir") workflow.target("TestTarget1", inputs=[], outputs=["/some/dir/test_output.txt"]) workflow.target("TestTarget2", inputs=[], outputs=["test_output.txt"]) with pytest.raises(WorkflowError): Graph.from_targets(workflow.targets)
def test_target_should_run_if_it_is_a_sink(self): target = Target('TestTarget', inputs=[], outputs=[], options={}, working_dir='/some/dir') graph = Graph(targets={'TestTarget': target}) with self.assertLogs(level='DEBUG') as logs: self.assertTrue(graph.should_run(target)) self.assertEqual( logs.output, [ 'DEBUG:gwf.core:TestTarget should run because it is a sink.' ] )
def test_two_targets_producing_the_same_file_but_declared_with_rel_and_abs_path(self, mock_os_path_exists): workflow = Workflow(working_dir='/some/dir') workflow.target('TestTarget1', inputs=[], outputs=['/some/dir/test_output.txt']) workflow.target('TestTarget2', inputs=[], outputs=['test_output.txt']) with self.assertRaises(FileProvidedByMultipleTargetsError): Graph(targets=workflow.targets)
def test_scheduling_unsubmitted_target(backend, monkeypatch): target = Target('TestTarget', inputs=[], outputs=[], options={}, working_dir='/some/dir') graph = Graph(targets={'TestTarget': target}) monkeypatch.setattr(graph, 'should_run', lambda t: True) assert schedule(graph, backend, target) == True assert len(backend.submit.call_args_list) == 1 assert call(target, dependencies=set()) in backend.submit.call_args_list
def test_scheduling_non_submitted_targets_that_should_not_run(backend, monkeypatch): target1 = Target( "TestTarget1", inputs=[], outputs=["test_output1.txt"], options={}, working_dir="/some/dir", ) target2 = Target( "TestTarget2", inputs=[], outputs=["test_output2.txt"], options={}, working_dir="/some/dir", ) target3 = Target( "TestTarget3", inputs=["test_output1.txt", "test_output2.txt"], outputs=["test_output3.txt"], options={}, working_dir="/some/dir", ) graph = Graph.from_targets( {"TestTarget1": target1, "TestTarget2": target2, "TestTarget3": target3} ) scheduler = Scheduler(graph=graph, backend=backend) monkeypatch.setattr(scheduler, "should_run", lambda t: False) assert scheduler.schedule(target3) == False assert backend.submit.call_args_list == []
def test_immediate_circular_dependencies_in_workflow_raises_exception(self, mock_os_path_exists): self.workflow.target( 'TestTarget1', inputs=['test_file2.txt'], outputs=['test_file1.txt']) self.workflow.target( 'TestTarget2', inputs=['test_file1.txt'], outputs=['test_file2.txt']) with self.assertRaises(CircularDependencyError): Graph(targets=self.workflow.targets)
def test_scheduling_non_submitted_targets_that_should_not_run(backend, monkeypatch): target1 = Target('TestTarget1', inputs=[], outputs=['test_output1.txt'], options={}, working_dir='/some/dir') target2 = Target('TestTarget2', inputs=[], outputs=['test_output2.txt'], options={}, working_dir='/some/dir') target3 = Target('TestTarget3', inputs=['test_output1.txt', 'test_output2.txt'], outputs=['test_output3.txt'], options={}, working_dir='/some/dir') graph = Graph(targets={'TestTarget1': target1, 'TestTarget2': target2, 'TestTarget3': target3}) monkeypatch.setattr(graph, 'should_run', lambda t: False) assert schedule(graph, backend, target3) == False assert backend.submit.call_args_list == []
def test_raises_exceptions_if_two_targets_produce_the_same_file(self): self.workflow.target( 'TestTarget1', inputs=[], outputs=['/test_output.txt'], working_dir='') self.workflow.target( 'TestTarget2', inputs=[], outputs=['/test_output.txt'], working_dir='') with self.assertRaises(FileProvidedByMultipleTargetsError): Graph(targets=self.workflow.targets)
def test_target_should_not_run_if_it_is_a_source_and_all_outputs_exist(self): workflow = Workflow(working_dir='/some/dir') target = workflow.target( 'TestTarget1', inputs=[], outputs=['test_output1.txt', 'test_output2.txt'] ) graph = Graph(targets=workflow.targets) mock_file_cache = { '/some/dir/test_output1.txt': 1, '/some/dir/test_output2.txt': 2 } with patch.dict(graph.file_cache, mock_file_cache): self.assertFalse( graph.should_run(target) )
def test_scheduling_target_with_deps_that_are_not_submitted(backend, monkeypatch): target1 = Target('TestTarget1', inputs=[], outputs=['test_output.txt'], options={}, working_dir='/some/dir') target2 = Target('TestTarget2', inputs=['test_output.txt'], outputs=[], options={}, working_dir='/some/dir') graph = Graph(targets={'TestTarget1': target1, 'TestTarget2': target2}) monkeypatch.setattr(graph, 'should_run', lambda t: True) assert schedule(graph, backend, target2) == True assert len(backend.submit.call_args_list) == 2 assert call(target1, dependencies=set()) in backend.submit.call_args_list assert call(target2, dependencies=set([target1])) in backend.submit.call_args_list
def test_scheduling_unsubmitted_target(backend, monkeypatch): target = Target( "TestTarget", inputs=[], outputs=[], options={}, working_dir="/some/dir" ) graph = Graph.from_targets({"TestTarget": target}) scheduler = Scheduler(graph=graph, backend=backend) monkeypatch.setattr(scheduler, "should_run", lambda t: True) assert scheduler.schedule(target) == True assert len(backend.submit.call_args_list) == 1 assert call(target, dependencies=set()) in backend.submit.call_args_list
def test_finds_non_existing_file_provided_by_other_target(self, mock_os_path_exists): target1 = self.workflow.target( 'TestTarget1', inputs=[], outputs=['test_file.txt']) target2 = self.workflow.target( 'TestTarget2', inputs=['test_file.txt'], outputs=[]) graph = Graph(targets=self.workflow.targets) self.assertIn(target2, graph.dependencies) self.assertIn(target1, graph.dependencies[target2])
def test_graph_raises_multiple_providers_error(): t1 = Target( name="Target1", inputs=[], outputs=["t1_output1.txt", "t1_output2.txt"], options={}, working_dir="/some/dir", ) t2 = Target( name="Target2", inputs=[], outputs=["t1_output2.txt", "t1_output3.txt"], options={}, working_dir="/some/dir", ) with pytest.raises(WorkflowError): Graph.from_targets({"Target1": t1, "Target2": t2})
def test_raise_error_if_two_targets_in_different_namespaces_produce_the_same_file(self): w1 = Workflow(name='foo') w1.target('SayHello', inputs=[], outputs=['greeting.txt']) w2 = Workflow(name='bar') w2.target('SayHi', inputs=[], outputs=['greeting.txt']) w2.include(w1) with self.assertRaises(FileProvidedByMultipleTargetsError): g = Graph(targets=w2.targets)
def test_scheduling_many_targets_calls_schedule_for_each_target(backend, monkeypatch): target1 = Target('TestTarget1', inputs=[], outputs=['test_output1.txt'], options={}, working_dir='/some/dir') target2 = Target('TestTarget2', inputs=[], outputs=['test_output2.txt'], options={}, working_dir='/some/dir') target3 = Target('TestTarget3', inputs=['test_output1.txt'], outputs=['test_output3.txt'], options={}, working_dir='/some/dir') target4 = Target('TestTarget4', inputs=['test_output2.txt'], outputs=['test_output4.txt'], options={}, working_dir='/some/dir') graph = Graph(targets={'TestTarget1': target1, 'TestTarget2': target2, 'TestTarget3': target3, 'TestTarget4': target4}) monkeypatch.setattr(graph, 'should_run', lambda t: True) assert schedule_many(graph, backend, [target3, target4]) == [True, True] assert call(target4, dependencies=set([target2])) in backend.submit.call_args_list assert call(target3, dependencies=set([target1])) in backend.submit.call_args_list assert call(target2, dependencies=set()) in backend.submit.call_args_list assert call(target1, dependencies=set()) in backend.submit.call_args_list
def test_existing_files_not_provided_by_other_target(backend): target = Target( "TestTarget", inputs=["test_input.txt"], outputs=[], options={}, working_dir="/some/dir", ) graph = Graph.from_targets({"TestTarget": target}) scheduler = Scheduler( graph=graph, backend=backend, file_cache={"/some/dir/test_input.txt": 0} ) assert scheduler.schedule(target) == True
def test_scheduling_branch_and_join_structure(backend, monkeypatch): target1 = Target('TestTarget1', inputs=[], outputs=['output1.txt'], options={}, working_dir='/some/dir') target2 = Target('TestTarget2', inputs=['output1.txt'], outputs=['output2.txt'], options={}, working_dir='/some/dir') target3 = Target('TestTarget3', inputs=['output1.txt'], outputs=['output3.txt'], options={}, working_dir='/some/dir') target4 = Target('TestTarget4', inputs=['output2.txt', 'output3.txt'], outputs=['final.txt'], options={}, working_dir='/some/dir') graph = Graph(targets={'target1': target1, 'target2': target2, 'target3': target3, 'target4': target4}) monkeypatch.setattr(graph, 'should_run', lambda t: True) assert schedule(graph, backend, target4) == True assert len(backend.submit.call_args_list) == 4 assert call(target1, dependencies=set([])) in backend.submit.call_args_list assert call(target2, dependencies=set([target1])) in backend.submit.call_args_list assert call(target3, dependencies=set([target1])) in backend.submit.call_args_list assert call(target4, dependencies=set([target3, target2])) in backend.submit.call_args_list
def test_dependencies_correctly_resolved_for_named_workflow(self): workflow = Workflow(name='foo') target1 = workflow.target('TestTarget1', inputs=[], outputs=['test.txt']) target2 = workflow.target('TestTarget2', inputs=['test.txt'], outputs=[]) other_workflow = Workflow(name='bar') other_workflow.include(workflow) other_target1 = other_workflow.target('TestTarget1', inputs=['test.txt'], outputs=[]) graph = Graph(targets=other_workflow.targets) assert 'TestTarget1' in graph.targets assert 'foo.TestTarget2' in graph.targets assert 'foo.TestTarget2' in graph.targets
def test_non_existing_files_not_provided_by_other_target(backend): target = Target( "TestTarget", inputs=["test_input.txt"], outputs=[], options={}, working_dir="/some/dir", ) graph = Graph.from_targets({"TestTarget": target}) scheduler = Scheduler( graph=graph, backend=backend, file_cache={"/some/dir/test_input.txt": None} ) with pytest.raises(WorkflowError): scheduler.schedule(target)
def test_target_should_not_run_if_it_is_a_source_and_all_outputs_exist(self): workflow = Workflow(working_dir="/some/dir") target = workflow.target( "TestTarget1", inputs=[], outputs=["test_output1.txt", "test_output2.txt"] ) graph = Graph.from_targets(workflow.targets) scheduler = Scheduler(graph=graph, backend=DummyBackend()) mock_file_cache = { "/some/dir/test_output1.txt": 1, "/some/dir/test_output2.txt": 2, } with patch.dict(scheduler._file_cache, mock_file_cache): self.assertFalse(scheduler.should_run(target))
def test_graph_raises_circular_dependency_error(): t1 = Target( name="Target1", inputs=["f1.txt"], outputs=["f2.txt"], options={}, working_dir="/some/dir", ) t2 = Target( name="Target2", inputs=["f2.txt"], outputs=["f3.txt"], options={}, working_dir="/some/dir", ) t3 = Target( name="Target3", inputs=["f3.txt"], outputs=["f1.txt"], options={}, working_dir="/some/dir", ) with pytest.raises(WorkflowError): Graph.from_targets({"Target1": t1, "Target2": t2, "Target3": t3})
def test_exception_if_input_file_is_not_provided_and_output_file_exists(): workflow = Workflow(working_dir="/some/dir") target = workflow.target("TestTarget", inputs=["in.txt"], outputs=["out.txt"]) graph = Graph.from_targets(workflow.targets) print(graph.unresolved) backend = DummyBackend() scheduler = Scheduler( graph=graph, backend=backend, file_cache={"/some/dir/in.txt": None, "/some/dir/out.txt": 1}, ) with pytest.raises(WorkflowError): scheduler.should_run(target)
def test_scheduling_branch_and_join_structure_with_previously_submitted_dependency( backend, monkeypatch ): target1 = Target( "TestTarget1", inputs=[], outputs=["output1.txt"], options={}, working_dir="/some/dir", ) target2 = Target( "TestTarget2", inputs=["output1.txt"], outputs=["output2.txt"], options={}, working_dir="/some/dir", ) target3 = Target( "TestTarget3", inputs=["output1.txt"], outputs=["output3.txt"], options={}, working_dir="/some/dir", ) target4 = Target( "TestTarget4", inputs=["output2.txt", "output3.txt"], outputs=["final.txt"], options={}, working_dir="/some/dir", ) graph = Graph.from_targets( {"target1": target1, "target2": target2, "target3": target3, "target4": target4} ) scheduler = Scheduler(graph=graph, backend=backend) monkeypatch.setattr(scheduler, "should_run", lambda t: True) backend.submit(target1, dependencies=set()) assert scheduler.schedule(target4) == True assert len(backend.submit.call_args_list) == 4 assert call(target2, dependencies=set([target1])) in backend.submit.call_args_list assert call(target3, dependencies=set([target1])) in backend.submit.call_args_list assert ( call(target4, dependencies=set([target3, target2])) in backend.submit.call_args_list )
def test_target_should_run_if_it_is_a_sink(self): target = Target( "TestTarget", inputs=[], outputs=[], options={}, working_dir="/some/dir" ) graph = Graph.from_targets({"TestTarget": target}) scheduler = Scheduler(graph=graph, backend=DummyBackend()) with self.assertLogs(level="DEBUG") as logs: self.assertTrue(scheduler.schedule(target)) self.assertEqual( logs.output, [ "DEBUG:gwf.core:Scheduling target TestTarget", "DEBUG:gwf.core:TestTarget should run because it is a sink", "INFO:gwf.core:Submitting target TestTarget", ], )
def test_scheduling_many_targets_calls_schedule_for_each_target(backend, monkeypatch): target1 = Target( "TestTarget1", inputs=[], outputs=["test_output1.txt"], options={}, working_dir="/some/dir", ) target2 = Target( "TestTarget2", inputs=[], outputs=["test_output2.txt"], options={}, working_dir="/some/dir", ) target3 = Target( "TestTarget3", inputs=["test_output1.txt"], outputs=["test_output3.txt"], options={}, working_dir="/some/dir", ) target4 = Target( "TestTarget4", inputs=["test_output2.txt"], outputs=["test_output4.txt"], options={}, working_dir="/some/dir", ) graph = Graph.from_targets( { "TestTarget1": target1, "TestTarget2": target2, "TestTarget3": target3, "TestTarget4": target4, } ) scheduler = Scheduler(graph=graph, backend=backend) monkeypatch.setattr(scheduler, "should_run", lambda t: True) assert scheduler.schedule_many([target3, target4]) == [True, True] assert call(target4, dependencies=set([target2])) in backend.submit.call_args_list assert call(target3, dependencies=set([target1])) in backend.submit.call_args_list assert call(target2, dependencies=set()) in backend.submit.call_args_list assert call(target1, dependencies=set()) in backend.submit.call_args_list
def setUp(self): workflow = Workflow(working_dir="/some/dir") self.target1 = workflow.target( "TestTarget1", inputs=[], outputs=["test_output1.txt"] ) self.target2 = workflow.target( "TestTarget2", inputs=["test_output1.txt"], outputs=["test_output2.txt"] ) self.target3 = workflow.target( "TestTarget3", inputs=["test_output1.txt"], outputs=["test_output3.txt"] ) self.target4 = workflow.target( "TestTarget4", inputs=["test_output2.txt", "test_output3.txt"], outputs=["final_output.txt"], ) self.graph = Graph.from_targets(workflow.targets) self.backend = DummyBackend() self.scheduler = Scheduler(graph=self.graph, backend=self.backend)
def test_scheduling_target_with_deep_deps_that_are_not_submitted(backend, monkeypatch): target1 = Target( "TestTarget1", inputs=[], outputs=["test_output1.txt"], options={}, working_dir="/some/dir", ) target2 = Target( "TestTarget2", inputs=["test_output1.txt"], outputs=["test_output2.txt"], options={}, working_dir="/some/dir", ) target3 = Target( "TestTarget3", inputs=["test_output2.txt"], outputs=["test_output3.txt"], options={}, working_dir="/some/dir", ) target4 = Target( "TestTarget4", inputs=["test_output3.txt"], outputs=["final_output.txt"], options={}, working_dir="/some/dir", ) graph = Graph.from_targets( {"target1": target1, "target2": target2, "target3": target3, "target4": target4} ) scheduler = Scheduler(graph=graph, backend=backend) monkeypatch.setattr(scheduler, "should_run", lambda t: True) assert scheduler.schedule(target4) == True assert len(backend.submit.call_args_list) == 4 assert call(target1, dependencies=set()) in backend.submit.call_args_list assert call(target2, dependencies=set([target1])) in backend.submit.call_args_list assert call(target3, dependencies=set([target2])) in backend.submit.call_args_list assert call(target4, dependencies=set([target3])) in backend.submit.call_args_list
def test_finds_no_providers_in_empty_workflow(self): graph = Graph(targets=self.workflow.targets) self.assertDictEqual(graph.provides, {})
def test_finds_provider_in_workflow_with_one_producer(self): self.workflow.target('TestTarget', inputs=[], outputs=['/test_output.txt'], working_dir='') graph = Graph(targets=self.workflow.targets) self.assertIn('/test_output.txt', graph.provides) self.assertEqual(graph.provides['/test_output.txt'].name, 'TestTarget')
def test_finds_no_dependencies_for_target_with_no_inputs(self): target = self.workflow.target('TestTarget', inputs=[], outputs=[]) graph = Graph(targets=self.workflow.targets) self.assertEqual(graph.dependencies[target], set())
def test_non_existing_files_not_provided_by_other_target_raises_exception(self, mock_os_path_exists): self.workflow.target( 'TestTarget', inputs=['test_input.txt'], outputs=[]) with self.assertRaises(FileRequiredButNotProvidedError): Graph(targets=self.workflow.targets,)
def test_existing_files_not_provided_by_other_target_has_no_dependencies(self, mock_exists): target = self.workflow.target('TestTarget', inputs=['test_file.txt'], outputs=[]) graph = Graph(targets=self.workflow.targets) self.assertEqual(graph.dependencies[target], set())
def test_raise_exception_if_output_file_is_a_dir(self): self.workflow.target('TestTarget1', inputs=[], outputs=['/tmp']) with self.assertRaises(GWFError): Graph(targets=self.workflow.targets)
def test_endpoints_only_includes_target_with_no_dependents(self): self.workflow.target('TestTarget1', inputs=[], outputs=['test.txt']) target2 = self.workflow.target('TestTarget2', inputs=['test.txt'], outputs=[]) target3 = self.workflow.target('TestTarget3', inputs=['test.txt'], outputs=[]) graph = Graph(targets=self.workflow.targets) self.assertSetEqual(graph.endpoints(), {target2, target3})
class TestShouldRun(unittest.TestCase): def setUp(self): workflow = Workflow(working_dir='/some/dir') self.target1 = workflow.target('TestTarget1', inputs=[], outputs=['test_output1.txt']) self.target2 = workflow.target('TestTarget2', inputs=['test_output1.txt'], outputs=['test_output2.txt']) self.target3 = workflow.target('TestTarget3', inputs=['test_output1.txt'], outputs=['test_output3.txt']) self.target4 = workflow.target('TestTarget4', inputs=['test_output2.txt', 'test_output3.txt'], outputs=['final_output.txt']) self.graph = Graph(targets=workflow.targets) def test_target_should_run_if_one_of_its_dependencies_does_not_exist(self): with self.assertLogs(level='DEBUG') as logs: self.assertTrue(self.graph.should_run(self.target1)) self.assertEqual( logs.output, [ 'DEBUG:gwf.core:TestTarget1 should run because one of its output files does not exist.' ] ) def test_target_should_run_if_one_of_its_dependencies_should_run(self): with self.assertLogs(level='DEBUG') as logs: self.assertTrue(self.graph.should_run(self.target2)) self.assertEqual( logs.output, [ 'DEBUG:gwf.core:TestTarget1 should run because one of its output files does not exist.', 'DEBUG:gwf.core:TestTarget2 should run because one of its dependencies should run.' ] ) def test_target_should_run_if_it_is_a_sink(self): target = Target('TestTarget', inputs=[], outputs=[], options={}, working_dir='/some/dir') graph = Graph(targets={'TestTarget': target}) with self.assertLogs(level='DEBUG') as logs: self.assertTrue(graph.should_run(target)) self.assertEqual( logs.output, [ 'DEBUG:gwf.core:TestTarget should run because it is a sink.' ] ) def test_target_should_not_run_if_it_is_a_source_and_all_outputs_exist(self): workflow = Workflow(working_dir='/some/dir') target = workflow.target( 'TestTarget1', inputs=[], outputs=['test_output1.txt', 'test_output2.txt'] ) graph = Graph(targets=workflow.targets) mock_file_cache = { '/some/dir/test_output1.txt': 1, '/some/dir/test_output2.txt': 2 } with patch.dict(graph.file_cache, mock_file_cache): self.assertFalse( graph.should_run(target) ) def test_should_run_if_any_input_file_is_newer_than_any_output_file(self): mock_file_cache = { '/some/dir/test_output1.txt': 0, '/some/dir/test_output2.txt': 1, '/some/dir/test_output3.txt': 3, '/some/dir/final_output.txt': 2, } with patch.dict(self.graph.file_cache, mock_file_cache): self.assertFalse(self.graph.should_run(self.target1)) self.assertFalse(self.graph.should_run(self.target2)) self.assertFalse(self.graph.should_run(self.target3)) self.assertTrue(self.graph.should_run(self.target4)) def test_should_run_not_run_if_all_outputs_are_newer_then_the_inputs(self): mock_file_cache = { '/some/dir/test_output1.txt': 0, '/some/dir/test_output2.txt': 1, '/some/dir/test_output3.txt': 3, '/some/dir/final_output.txt': 4, } with patch.dict(self.graph.file_cache, mock_file_cache): self.assertFalse(self.graph.should_run(self.target1)) self.assertFalse(self.graph.should_run(self.target2)) self.assertFalse(self.graph.should_run(self.target3)) self.assertFalse(self.graph.should_run(self.target4)) @patch('gwf.core.os.path.exists', return_value=True, autospec=True) def test_two_targets_producing_the_same_file_but_declared_with_rel_and_abs_path(self, mock_os_path_exists): workflow = Workflow(working_dir='/some/dir') workflow.target('TestTarget1', inputs=[], outputs=['/some/dir/test_output.txt']) workflow.target('TestTarget2', inputs=[], outputs=['test_output.txt']) with self.assertRaises(FileProvidedByMultipleTargetsError): Graph(targets=workflow.targets)
def test_finds_no_providers_in_workflow_with_no_producers(self): self.workflow.target('TestTarget', inputs=[], outputs=[]) graph = Graph(targets=self.workflow.targets) self.assertDictEqual(graph.provides, {})
def test_build_branch_join_graph(): t1 = Target( name="Target1", inputs=["test_input1.txt", "test_input2.txt"], outputs=["t1_output1.txt", "t1_output2.txt"], options={}, working_dir="/some/dir", ) t2 = Target( name="Target2", inputs=["t1_output1.txt"], outputs=["t2_output.txt"], options={}, working_dir="/some/dir", ) t3 = Target( name="Target3", inputs=["t1_output2.txt"], outputs=["t3_output.txt"], options={}, working_dir="/some/dir", ) t4 = Target( name="Target4", inputs=["t2_output.txt", "t3_output.txt"], outputs=["t4_output.txt"], options={}, working_dir="/some/dir", ) targets = {"Target1": t1, "Target2": t2, "Target3": t3, "Target4": t4} graph = Graph.from_targets(targets) assert len(graph.targets) == 4 assert not graph.dependencies[t1] assert graph.dependents[t1] == {t2, t3} assert graph.dependencies[t2] == {t1} assert graph.dependents[t2] == {t4} assert graph.dependencies[t3] == {t1} assert graph.dependents[t3] == {t4} assert graph.dependencies[t4] == {t2, t3} assert graph.dependents[t4] == set() assert graph.provides["/some/dir/t1_output1.txt"] == t1 assert graph.provides["/some/dir/t1_output2.txt"] == t1 assert graph.provides["/some/dir/t2_output.txt"] == t2 assert graph.provides["/some/dir/t3_output.txt"] == t3 assert graph.provides["/some/dir/t4_output.txt"] == t4 assert graph.unresolved == { "/some/dir/test_input1.txt", "/some/dir/test_input2.txt", }