Repository: siliconcompiler/siliconcompiler

Issue:
_select_inputs need to return the pruned nodes
Referring to #3235 discussion and particularly this [point](https://github.com/siliconcompiler/siliconcompiler/discussions/3235#discussioncomment-12279101). 

I managed to get that issue in a test, with a modified `builtin/nop`. 

<details>

<summary>siliconcompiler/tools/builtin/nop.py</summary>

```python
from siliconcompiler.tools.builtin import _common
from siliconcompiler.tools._common import get_tool_task
from siliconcompiler.tools.builtin.builtin import set_io_files
from siliconcompiler import flowgraph


def setup(chip):
    '''
    A no-operation that passes inputs to outputs.
    '''

    set_io_files(chip)

# Commented this out, because this was being used in scheduler._select_inputs. 
# This function also changes the inputs to prune_inputs, which was preventing the error to appear. 
# def _select_inputs(chip, step, index):
#     return _common._select_inputs(chip, step, index)


def _gather_outputs(chip, step, index):
    '''Return set of filenames that are guaranteed to be in outputs
    directory after a successful run of step/index.'''

    flow = chip.get('option', 'flow')

    in_nodes = chip.get('flowgraph', flow, step, index, 'input')
    in_task_outputs = [flowgraph._gather_outputs(chip, *node) for node in in_nodes]

    if len(in_task_outputs) > 0:
        return in_task_outputs[0].union(*in_task_outputs[1:])

    return []


def run(chip):
    return _common.run(chip)


def post_process(chip):
    _common.post_process(chip)


```

</details>


<details>

<summary>tests/flows/test_prune.py</summary>


``` python

def test_input_provides_with_prune_testcase_failing():
    chip = siliconcompiler.Chip('test')
    flow_name = 'test_flow'

    flow = siliconcompiler.Flow(flow_name)
    flow.node(flow_name, 'initial', nop)
    flow.node(flow_name, 'onestep', nop)
    flow.node(flow_name, 'twostep', nop)
    flow.node(flow_name, 'finalstep', nop)
    flow.edge(flow_name, 'initial', 'onestep')
    flow.edge(flow_name, 'initial', 'twostep')
    flow.edge(flow_name, 'onestep', 'finalstep')
    flow.edge(flow_name, 'twostep', 'finalstep')

    chip.use(flow)
    chip.set('option', 'flow', flow_name)
    chip.set('tool', 'builtin', 'task', 'nop', 'output', 'test.initial.txt', step='initial', index='0')
    chip.set('tool', 'builtin', 'task', 'nop', 'output', 'test.one.txt', step='onestep', index='0')
    chip.set('tool', 'builtin', 'task', 'nop', 'output', 'test.two.txt', step='twostep', index='0')

    chip.set('option', 'prune', ('onestep', '0'))

    assert chip.run()

```
</details>

Output:

```text

| INFO    | job0 | --        | - | Setting up node initial0 with builtin/nop
| INFO    | job0 | --        | - | Setting up node onestep0 with builtin/nop
| INFO    | job0 | --        | - | Setting up node twostep0 with builtin/nop
| INFO    | job0 | --        | - | Setting up node finalstep0 with builtin/nop
| INFO    | job0 | --        | - | Checking manifest before running.
| INFO    | job0 | initial   | 0 | Running in /tmp/pytest-of-ciprian/pytest-65/test_input_provides_with_prune0/build/test/job0/initial/0
| INFO    | job0 | initial   | 0 | Finished task in 0.94s
| INFO    | job0 | twostep   | 0 | Running in /tmp/pytest-of-ciprian/pytest-65/test_input_provides_with_prune0/build/test/job0/twostep/0
| INFO    | job0 | twostep   | 0 | Finished task in 0.98s
| ERROR   | job0 | finalstep | 0 | Unable to locate outputs directory for onestep0: /tmp/pytest-of-ciprian/pytest-65/test_input_provides_with_prune0/build/test/job0/onestep/0/outputs
| ERROR   | job0 | finalstep | 0 | Halting step 'finalstep' index '0' due to errors.
| ERROR   | job0 | --        | - | These final steps could not be reached: ['finalstep']
| ERROR   | job0 | --        | - | These final steps could not be reached: ['finalstep']

```


So the tests were not capturing this error because of the `_common._select_inputs`, which is returning the `pruned nodes`. 

```python
def _select_inputs(chip, step, index):
    _, task = get_tool_task(chip, step, index)

    chip.logger.info(f"Running builtin task '{task}'")

    flow = chip.get('option', 'flow')
    return _get_pruned_node_inputs(chip, flow, (step, index))
```

The question is, shouldn't this be the default behavior in `scheduler._select_inputs`?

![Image](https://github.com/user-attachments/assets/e8a5fb63-79fb-440d-a7d3-219060881790)

Hints from issue comments:
@gadfort: You are correct, thanks for bringing that up, I fixed the behavior and added a test to ensure the proper behavior is honored.@Ciprian167: You're fast! Thank you. 

Solution patch:
diff --git a/siliconcompiler/scheduler/__init__.py b/siliconcompiler/scheduler/__init__.py
index c69279558..bebe245ff 100644
--- a/siliconcompiler/scheduler/__init__.py
+++ b/siliconcompiler/scheduler/__init__.py
@@ -27,7 +27,7 @@ from siliconcompiler.scheduler import slurm
 from siliconcompiler.scheduler import docker_runner
 from siliconcompiler import NodeStatus, SiliconCompilerError
 from siliconcompiler.flowgraph import _get_flowgraph_nodes, _get_flowgraph_execution_order, \
-    _get_pruned_node_inputs, _get_flowgraph_node_inputs, _get_flowgraph_entry_nodes, \
+    _get_pruned_node_inputs, _get_flowgraph_entry_nodes, \
     _unreachable_steps_to_execute, _nodes_to_execute, \
     get_nodes_from, nodes_to_execute, _check_flowgraph
 from siliconcompiler.tools._common import input_file_node_name
@@ -562,7 +562,7 @@ def _select_inputs(chip, step, index, trial=False):
         if log_handlers:
             chip.logger.handlers = log_handlers
     else:
-        sel_inputs = _get_flowgraph_node_inputs(chip, flow, (step, index))
+        sel_inputs = _get_pruned_node_inputs(chip, flow, (step, index))
 
     if (step, index) not in _get_flowgraph_entry_nodes(chip, flow) and not sel_inputs:
         chip.logger.error(f'No inputs selected after running {tool}')


Test patch:
diff --git a/tests/flows/test_prune.py b/tests/flows/test_prune.py
index acfda1dff..08323a18c 100644
--- a/tests/flows/test_prune.py
+++ b/tests/flows/test_prune.py
@@ -153,6 +153,7 @@ def test_input_provides_with_prune_multirun():
     chip.set('option', 'prune', ('onestep', '0'))
     assert chip.run()
     assert chip.get('record', 'status', step='finalstep', index='0') == "success"
+    assert chip.get('record', 'inputnode', step='finalstep', index='0') == [('twostep', '0')]
     end_time = chip.get('record', 'endtime', step='finalstep', index='0')
 
     # Add delay to ensure end time is different
@@ -161,6 +162,7 @@ def test_input_provides_with_prune_multirun():
     chip.set('option', 'prune', ('twostep', '0'))
     assert chip.run()
     assert chip.get('record', 'status', step='finalstep', index='0') == "success"
+    assert chip.get('record', 'inputnode', step='finalstep', index='0') == [('onestep', '0')]
     assert chip.get('record', 'endtime', step='finalstep', index='0') != end_time
     end_time = chip.get('record', 'endtime', step='finalstep', index='0')
 
@@ -170,6 +172,55 @@ def test_input_provides_with_prune_multirun():
     chip.unset('option', 'prune')
     assert chip.run()
     assert chip.get('record', 'status', step='finalstep', index='0') == "success"
+    assert set(chip.get('record', 'inputnode', step='finalstep', index='0')) == \
+        set([('onestep', '0'), ('twostep', '0')])
+    assert chip.get('record', 'endtime', step='finalstep', index='0') != end_time
+
+
+def test_input_provides_with_prune_multirun_default_select_inputs(monkeypatch):
+    # Remove use of builtin's _select_inputs to test the default _select_inputs
+    monkeypatch.delattr(nop, '_select_inputs')
+
+    chip = siliconcompiler.Chip('test')
+    flow_name = 'test_flow'
+
+    flow = siliconcompiler.Flow(flow_name)
+    flow.node(flow_name, 'initial', nop)
+    flow.node(flow_name, 'onestep', nop)
+    flow.node(flow_name, 'twostep', nop)
+    flow.node(flow_name, 'finalstep', nop)
+    flow.edge(flow_name, 'initial', 'onestep')
+    flow.edge(flow_name, 'initial', 'twostep')
+    flow.edge(flow_name, 'onestep', 'finalstep')
+    flow.edge(flow_name, 'twostep', 'finalstep')
+
+    chip.use(flow)
+    chip.set('option', 'flow', flow_name)
+
+    chip.set('option', 'prune', ('onestep', '0'))
+    assert chip.run()
+    assert chip.get('record', 'status', step='finalstep', index='0') == "success"
+    assert chip.get('record', 'inputnode', step='finalstep', index='0') == [('twostep', '0')]
+    end_time = chip.get('record', 'endtime', step='finalstep', index='0')
+
+    # Add delay to ensure end time is different
+    time.sleep(2)
+
+    chip.set('option', 'prune', ('twostep', '0'))
+    assert chip.run()
+    assert chip.get('record', 'status', step='finalstep', index='0') == "success"
+    assert chip.get('record', 'inputnode', step='finalstep', index='0') == [('onestep', '0')]
+    assert chip.get('record', 'endtime', step='finalstep', index='0') != end_time
+    end_time = chip.get('record', 'endtime', step='finalstep', index='0')
+
+    # Add delay to ensure end time is different
+    time.sleep(2)
+
+    chip.unset('option', 'prune')
+    assert chip.run()
+    assert chip.get('record', 'status', step='finalstep', index='0') == "success"
+    assert set(chip.get('record', 'inputnode', step='finalstep', index='0')) == \
+        set([('onestep', '0'), ('twostep', '0')])
     assert chip.get('record', 'endtime', step='finalstep', index='0') != end_time