예제 #1
0
def full_bout_runner_tester(
    submitter_type: Type[AbstractSubmitter],
    make_project: Path,
    yield_number_of_rows_for_all_tables: Callable[[DatabaseReader], Dict[str, int]],
    file_state_restorer: FileStateRestorer,
) -> None:
    """
    Test that the BoutRunner can execute a run.

    This test will test that:
    1. We can execute a run
    2. The metadata is properly stored

    Parameters
    ----------
    submitter_type : type
        Submitter type to check for
    make_project : Path
        The path to the conduction example
    yield_number_of_rows_for_all_tables : function
        Function which returns the number of rows for all tables in a schema
    file_state_restorer : FileStateRestorer
        Object for restoring files to original state
    """
    name = "test_bout_runner_integration"
    run_group = make_run_group(
        {"name": name, "run_graph": None, "waiting_for": None},
        make_project,
        file_state_restorer,
    )
    # Run the project
    runner = BoutRunner(run_group.run_graph)
    runner.run()
    runner.wait_until_completed()

    assert isinstance(
        runner.run_graph["bout_run_test_bout_runner_integration"]["submitter"],
        submitter_type,
    )

    # Assert that the run went well
    db_reader = assert_first_run(
        run_group.bout_paths,
        run_group.db_connector,
    )
    # Assert that all the values are 1
    assert_tables_have_expected_len(
        db_reader, yield_number_of_rows_for_all_tables, expected_run_number=1
    )
예제 #2
0
def test_full_bout_runner(
    make_project: Path,
    yield_number_of_rows_for_all_tables: Callable[[DatabaseReader], Dict[str,
                                                                         int]],
    clean_default_db_dir: Path,
) -> None:
    """
    Test that the BoutRunner can execute a run.

    This test will test that:
    1. We can execute a run
    2. The metadata is properly stored

    Parameters
    ----------
    make_project : Path
        The path to the conduction example
    yield_number_of_rows_for_all_tables : function
        Function which returns the number of rows for all tables in a schema
    clean_default_db_dir : Path
        Path to the default database directory
    """
    _ = clean_default_db_dir
    name = "test_bout_runner_integration"
    run_group = make_run_group(name, make_project)

    # Run the project
    runner = BoutRunner(run_group.run_graph)
    runner.run()

    # Assert that the run went well
    db_reader = assert_first_run(
        run_group.bout_paths,
        run_group.db_connector,
    )
    # Assert that all the values are 1
    assert_tables_have_expected_len(db_reader,
                                    yield_number_of_rows_for_all_tables,
                                    expected_run_number=1)
예제 #3
0
def large_graph_tester(
    submitter_type: Type[AbstractSubmitter],
    make_project: Path,
    yield_number_of_rows_for_all_tables: Callable[[DatabaseReader], Dict[str, int]],
    file_state_restorer: FileStateRestorer,
) -> None:
    """
    Test that the graph with 10 nodes work as expected.

    The node setup can be found in node_functions.py

    Parameters
    ----------
    submitter_type : type
        Used to assert that the correct submitter is used
    make_project : Path
        The path to the conduction example
    yield_number_of_rows_for_all_tables : function
        Function which returns the number of rows for all tables in a schema
    file_state_restorer : FileStateRestorer
        Object for restoring files to original state
    """
    name = f"test_large_graph_{submitter_type.__name__}"

    node_adder = LargeGraphNodeAdder(
        name, make_project, submitter_type, file_state_restorer
    )
    # RunGroup belonging to node 2
    node_adder.add_and_assert_node_group_2()
    # RunGroup belonging to node 3 and 4
    node_adder.add_and_assert_node_group_3_and_4()
    # RunGroup belonging to node 6
    node_8 = node_adder.add_and_assert_node_group_6()
    # RunGroup belonging to node 9
    node_adder.add_and_assert_node_node_9(node_8)
    # Run the project
    runner = BoutRunner(node_adder.run_graph)
    runner.run()
    runner.wait_until_completed()
    # Check that all the nodes have changed status
    with pytest.raises(RuntimeError):
        runner.run()
        runner.wait_until_completed()
    # Check that all files are present
    # Check that the pre and post files are present
    for node in (0, 1, 5, 7, 8, 10):
        assert (
            node_adder.paths["pre_and_post_directory"].joinpath(f"{node}.txt").is_file()
        )
    # Check that all the dump files are present
    for restart_str in ("", "_restart_0", "_restart_1", "_restart_2"):
        assert (
            node_adder.paths["project_path"]
            .joinpath(f"{name}{restart_str}", "BOUT.dmp.0.nc")
            .is_file()
            or node_adder.paths["project_path"]
            .joinpath(f"{name}{restart_str}", "BOUT.dmp.0.h5")
            .is_file()
        )
    # NOTE: We will only have 4 runs as node 4 is a duplicate of node 2 and will
    #       therefore be skipped
    number_of_runs = 4
    assert_tables_have_expected_len(
        DatabaseReader(node_adder.run_groups["run_group_2"].db_connector),
        yield_number_of_rows_for_all_tables,
        expected_run_number=number_of_runs,
        restarted=True,
    )
    simulation_steps = LogReader(
        node_adder.paths["project_path"].joinpath(f"{name}_restart_2", "BOUT.log.0")
    ).get_simulation_steps()
    # NOTE: nout=0 set in the function tests.utils.run.make_run_group
    assert np.isclose(simulation_steps.loc[simulation_steps.index[-1], "Sim_time"], 0.0)
예제 #4
0
def bout_runner_from_path_tester(
    submitter_type: Type[AbstractSubmitter],
    project_path: Path,
    yield_number_of_rows_for_all_tables: Callable[[DatabaseReader], Dict[str, int]],
    file_state_restorer: FileStateRestorer,
) -> None:
    """
    Test that the minimal BoutRunners setup works.

    This test will test that:
    1. We can execute a run from the (mocked) current work directory
    2. The correct submitter has been used
    3. The metadata is properly stored
    4. We cannot execute the run again...
    5. ...unless we set force=True
    6. Check the restart functionality twice

    Parameters
    ----------
    submitter_type : type
        Submitter type to check for
    project_path : Path
        The path to the conduction example
    yield_number_of_rows_for_all_tables : function
        Function which returns the number of rows for all tables in a schema
    file_state_restorer : FileStateRestorer
        Object for restoring files to original state
    """
    # NOTE: This triggers too-many-statements (51/50)
    # pylint: disable=too-many-statements
    logging.info("Start: First run")
    # Make project to save time
    _ = project_path
    file_state_restorer.add(project_path.joinpath("conduction.db"))
    file_state_restorer.add(project_path.joinpath("settings_run"))
    with change_directory(project_path):
        runner = BoutRunner()
        bout_run_setup = runner.run_graph["bout_run_0"]["bout_run_setup"]
        file_state_restorer.add(
            bout_run_setup.bout_paths.bout_inp_dst_dir, force_mark_removal=True
        )
    runner.run()
    runner.wait_until_completed()

    assert isinstance(bout_run_setup.executor.submitter, submitter_type)

    bout_paths = bout_run_setup.bout_paths
    db_connector = bout_run_setup.db_connector
    # Assert that the run went well
    db_reader = assert_first_run(bout_paths, db_connector)
    # Assert that the number of runs is 1
    assert_tables_have_expected_len(
        db_reader, yield_number_of_rows_for_all_tables, expected_run_number=1
    )
    logging.info("Done: First run")

    logging.info("Start: Check RuntimeError")
    # Check that all the nodes have changed status
    with pytest.raises(RuntimeError):
        runner.run()
        runner.wait_until_completed()
    logging.info("Done: Check RuntimeError")
    logging.info("Start: Assert that run will not be run again")
    # Check that the run will not be executed again
    runner.reset()
    runner.run()
    runner.wait_until_completed()
    # Assert that the number of runs is 1
    assert_tables_have_expected_len(
        db_reader, yield_number_of_rows_for_all_tables, expected_run_number=1
    )
    logging.info("Done: Assert that run will not be run again")
    logging.info("Start: Run with force=True")
    # Check that force overrides the behaviour
    runner.run(force=True)
    runner.wait_until_completed()
    assert_tables_have_expected_len(
        db_reader, yield_number_of_rows_for_all_tables, expected_run_number=2
    )
    logging.info("Done: Run with force=True")

    logging.info("Start: Run with restart_all=True the first time")
    dump_dir_parent = bout_paths.bout_inp_dst_dir.parent
    dump_dir_name = bout_paths.bout_inp_dst_dir.name
    file_state_restorer.add(dump_dir_parent.joinpath(f"{dump_dir_name}_restart_0"))

    # Check that the restart functionality works
    runner.run(restart_all=True)
    runner.wait_until_completed()
    assert_tables_have_expected_len(
        db_reader,
        yield_number_of_rows_for_all_tables,
        expected_run_number=3,
        restarted=True,
    )
    # NOTE: The test in tests.unit.bout_runners.runners.test_bout_runner is testing
    #       restart_from_bout_inp_dst=True, whether this is testing restart_all=True
    assert_dump_files_exist(dump_dir_parent.joinpath(f"{dump_dir_name}_restart_0"))
    logging.info("Done: Run with restart_all=True the first time")
    logging.info("Start: Run with restart_all=True the second time")
    # ...twice
    file_state_restorer.add(dump_dir_parent.joinpath(f"{dump_dir_name}_restart_1"))
    runner.run(restart_all=True)
    runner.wait_until_completed()
    assert_tables_have_expected_len(
        db_reader,
        yield_number_of_rows_for_all_tables,
        expected_run_number=4,
        restarted=True,
    )
    # NOTE: The test in tests.unit.bout_runners.runners.test_bout_runner is testing
    #       restart_from_bout_inp_dst=True, whether this is testing restart_all=True
    assert_dump_files_exist(dump_dir_parent.joinpath(f"{dump_dir_name}_restart_1"))
    simulation_steps = LogReader(
        dump_dir_parent.joinpath(f"{dump_dir_name}_restart_1", "BOUT.log.0")
    ).get_simulation_steps()
    assert np.isclose(
        simulation_steps.loc[simulation_steps.index[-1], "Sim_time"], 30.0
    )
    logging.info("Done: Run with restart_all=True the second time")
예제 #5
0
def test_bout_runners_from_directory(
    make_project: Path,
    yield_number_of_rows_for_all_tables: Callable[[DatabaseReader], Dict[str,
                                                                         int]],
    clean_default_db_dir: Path,
    tear_down_restart_directories: Callable[[Path], None],
) -> None:
    """
    Test that the minimal BoutRunners setup works.

    This test will test that:
    1. We can execute a run from the (mocked) current work directory
    2. The metadata is properly stored
    3. We cannot execute the run again...
    4. ...unless we set force=True
    5. Check the restart functionality twice

    Parameters
    ----------
    make_project : Path
        The path to the conduction example
    yield_number_of_rows_for_all_tables : function
        Function which returns the number of rows for all tables in a schema
    clean_default_db_dir : Path
        Path to the default database directory
    tear_down_restart_directories : function
        Function used for removal of restart directories
    """
    # For automatic clean-up
    _ = clean_default_db_dir
    # Make project to save time
    project_path = make_project
    with change_directory(project_path):
        runner = BoutRunner()
        bout_run_setup = runner.run_graph["bout_run_0"]["bout_run_setup"]

    runner.run()

    bout_paths = bout_run_setup.bout_paths
    tear_down_restart_directories(bout_run_setup.bout_paths.bout_inp_dst_dir)
    db_connector = bout_run_setup.db_connector
    # Assert that the run went well
    db_reader = assert_first_run(bout_paths, db_connector)
    # Assert that the number of runs is 1
    assert_tables_have_expected_len(db_reader,
                                    yield_number_of_rows_for_all_tables,
                                    expected_run_number=1)

    # Check that all the nodes have changed status
    with pytest.raises(RuntimeError):
        runner.run()

    # Check that the run will not be executed again
    runner.reset()
    runner.run()
    # Assert that the number of runs is 1
    assert_tables_have_expected_len(db_reader,
                                    yield_number_of_rows_for_all_tables,
                                    expected_run_number=1)

    # Check that force overrides the behaviour
    runner.run(force=True)
    assert_tables_have_expected_len(db_reader,
                                    yield_number_of_rows_for_all_tables,
                                    expected_run_number=2)
    dump_dir_parent = bout_paths.bout_inp_dst_dir.parent
    dump_dir_name = bout_paths.bout_inp_dst_dir.name

    # Check that the restart functionality works
    runner.run(restart_all=True)
    expected_run_number = 3
    assert_tables_have_expected_len(
        db_reader,
        yield_number_of_rows_for_all_tables,
        expected_run_number=expected_run_number,
        restarted=True,
    )
    # NOTE: The test in tests.unit.bout_runners.runners.test_bout_runner is testing
    #       restart_from_bout_inp_dst=True, whether this is testing restart_all=True
    assert_dump_files_exist(
        dump_dir_parent.joinpath(f"{dump_dir_name}_restart_0"))
    # ...twice
    runner.run(restart_all=True)
    expected_run_number = 4
    assert_tables_have_expected_len(
        db_reader,
        yield_number_of_rows_for_all_tables,
        expected_run_number=expected_run_number,
        restarted=True,
    )
    # NOTE: The test in tests.unit.bout_runners.runners.test_bout_runner is testing
    #       restart_from_bout_inp_dst=True, whether this is testing restart_all=True
    assert_dump_files_exist(
        dump_dir_parent.joinpath(f"{dump_dir_name}_restart_1"))
예제 #6
0
def test_large_graph(
    make_project: Path,
    yield_number_of_rows_for_all_tables: Callable[[DatabaseReader], Dict[str,
                                                                         int]],
    clean_default_db_dir: Path,
    tear_down_restart_directories: Callable[[Path], None],
) -> None:
    """
    Test that the graph with 10 nodes work as expected.

    The node setup can be found in node_functions.py

    Parameters
    ----------
    make_project : Path
        The path to the conduction example
    yield_number_of_rows_for_all_tables : function
        Function which returns the number of rows for all tables in a schema
    clean_default_db_dir : Path
        Path to the default database directory
    tear_down_restart_directories : function
        Function used for removal of restart directories
    """
    _ = clean_default_db_dir

    name = "test_large_graph"
    paths = dict()
    paths["project_path"] = make_project
    paths["pre_and_post_directory"] = paths["project_path"].joinpath(
        f"pre_and_post_{name}")
    paths["pre_and_post_directory"].mkdir()
    run_groups = dict()

    # RunGroup belonging to node 2
    run_groups["run_group_2"] = make_run_group(name, make_project)
    run_graph = run_groups["run_group_2"].run_graph
    paths["bout_run_directory_node_2"] = run_groups[
        "run_group_2"].bout_paths.bout_inp_dst_dir

    run_groups["run_group_2"].add_pre_processor({
        "function":
        node_zero,
        "args": (
            paths["bout_run_directory_node_2"],
            paths["pre_and_post_directory"],
        ),
        "kwargs":
        None,
    })
    run_groups["run_group_2"].add_pre_processor({
        "function":
        node_one,
        "args": (
            paths["bout_run_directory_node_2"],
            paths["pre_and_post_directory"],
        ),
        "kwargs":
        None,
    })
    run_groups["run_group_2"].add_post_processor({
        "function":
        node_five,
        "args": (
            paths["bout_run_directory_node_2"],
            paths["pre_and_post_directory"],
        ),
        "kwargs":
        None,
    })

    tear_down_restart_directories(paths["bout_run_directory_node_2"])

    # RunGroup belonging to node 3
    run_groups["run_group_3"] = make_run_group(
        name,
        make_project,
        run_graph,
        restart_from=run_groups["run_group_2"].bout_paths.bout_inp_dst_dir,
        waiting_for=run_groups["run_group_2"].bout_run_node_name,
    )

    # RunGroup belonging to node 4
    run_groups["run_group_4"] = make_run_group(name, make_project, run_graph)
    paths["bout_run_directory_node_4"] = run_groups[
        "run_group_4"].bout_paths.bout_inp_dst_dir

    # RunGroup belonging to node 6
    run_groups["run_group_6"] = make_run_group(
        name,
        make_project,
        run_graph,
        restart_from=run_groups["run_group_2"].bout_paths.bout_inp_dst_dir,
        waiting_for=run_groups["run_group_2"].bout_run_node_name,
    )
    paths["bout_run_directory_node_6"] = run_groups[
        "run_group_6"].bout_paths.bout_inp_dst_dir
    node_8 = run_groups["run_group_6"].add_post_processor(
        {
            "function":
            node_eight,
            "args": (
                paths["bout_run_directory_node_4"],
                paths["bout_run_directory_node_6"],
                paths["pre_and_post_directory"],
            ),
            "kwargs":
            None,
        },
        waiting_for=run_groups["run_group_4"].bout_run_node_name,
    )

    # RunGroup belonging to node 9
    # NOTE: We need the paths['bout_run_directory_node_9'] as an input in node 7
    #       As node 9 is waiting for node 7 we hard-code the name
    #       (as we will know what it will be)
    paths["bout_run_directory_node_9"] = paths["project_path"].joinpath(
        f"{name}_restart_2")
    # The function of node_seven belongs to RunGroup2, but takes
    # paths['bout_run_directory_node_9'] as an input
    node_7_name = run_groups["run_group_2"].add_post_processor({
        "function":
        node_seven,
        "args": (
            paths["bout_run_directory_node_2"],
            paths["bout_run_directory_node_9"],
            paths["pre_and_post_directory"],
        ),
        "kwargs":
        None,
    })
    run_groups["run_group_9"] = make_run_group(
        name,
        make_project,
        run_graph,
        restart_from=run_groups["run_group_6"].bout_paths.bout_inp_dst_dir,
        waiting_for=(
            run_groups["run_group_4"].bout_run_node_name,
            run_groups["run_group_6"].bout_run_node_name,
            node_7_name,
        ),
    )
    run_groups["run_group_9"].add_post_processor(
        {
            "function":
            node_ten,
            "args": (
                paths["bout_run_directory_node_9"],
                paths["pre_and_post_directory"],
            ),
            "kwargs":
            None,
        },
        waiting_for=node_8,
    )

    # Run the project
    runner = BoutRunner(run_graph)
    runner.run()

    # Check that all the nodes have changed status
    with pytest.raises(RuntimeError):
        runner.run()

    # Check that all files are present
    # Check that the pre and post files are present
    for node in (0, 1, 5, 7, 8, 10):
        assert paths["pre_and_post_directory"].joinpath(
            f"{node}.txt").is_file()
    # Check that all the dump files are present
    for restart_str in ("", "_restart_0", "_restart_1", "_restart_2"):
        assert (paths["project_path"].joinpath(
            f"{name}{restart_str}").joinpath("BOUT.dmp.0.nc").is_file()
                or paths["project_path"].joinpath(f"{name}{restart_str}").
                joinpath("BOUT.dmp.0.h5").is_file())

    # NOTE: We will only have 4 runs as node 4 is a duplicate of node 2 and will
    #       therefore be skipped
    number_of_runs = 4
    assert_tables_have_expected_len(
        DatabaseReader(run_groups["run_group_2"].db_connector),
        yield_number_of_rows_for_all_tables,
        expected_run_number=number_of_runs,
        restarted=True,
    )
예제 #7
0
def test_run_bout_run(
    make_project: Path,
    clean_default_db_dir: Path,
    get_bout_run_setup: Callable[[str], BoutRunSetup],
    yield_number_of_rows_for_all_tables: Callable[[DatabaseReader], Dict[str,
                                                                         int]],
    tear_down_restart_directories: Callable[[Path], None],
) -> None:
    """
    Test the BOUT++ run method.

    Parameters
    ----------
    make_project : Path
        The path to the conduction example
    clean_default_db_dir : Path
        Path to the default database dir
    get_bout_run_setup : function
        Function which returns the BoutRunSetup object based on the conduction directory
    yield_number_of_rows_for_all_tables : function
        Function which returns the number of rows for all tables in a schema
    tear_down_restart_directories : function
        Function used for removal of restart directories
    """
    # For automatic clean-up
    _ = clean_default_db_dir
    # Make project to save time
    _ = make_project

    run_graph = RunGraph()
    runner = BoutRunner(run_graph)

    bout_run_setup = get_bout_run_setup("test_run_bout_run")
    tear_down_restart_directories(bout_run_setup.bout_paths.bout_inp_dst_dir)
    bout_paths = bout_run_setup.bout_paths
    db_connector = bout_run_setup.db_connector

    # Run once
    submitter = runner.run_bout_run(bout_run_setup)
    if submitter is not None:
        submitter.wait_until_completed()
    # Assert that the run went well
    database_reader = assert_first_run(bout_paths, db_connector)
    # Assert that the number of runs is 1
    assert_tables_have_expected_len(database_reader,
                                    yield_number_of_rows_for_all_tables,
                                    expected_run_number=1)

    # Check that the run will not be executed again
    assert runner.run_bout_run(bout_run_setup) is None
    # Assert that the number of runs is 1
    assert_tables_have_expected_len(database_reader,
                                    yield_number_of_rows_for_all_tables,
                                    expected_run_number=1)

    # Check that force overrides the behaviour
    submitter = runner.run_bout_run(bout_run_setup, force=True)
    if submitter is not None:
        submitter.wait_until_completed()
    assert_tables_have_expected_len(database_reader,
                                    yield_number_of_rows_for_all_tables,
                                    expected_run_number=2)
    dump_dir_parent = bout_paths.bout_inp_dst_dir.parent
    dump_dir_name = bout_paths.bout_inp_dst_dir.name

    # Check that restart makes another entry
    submitter = runner.run_bout_run(bout_run_setup,
                                    restart_from_bout_inp_dst=True)
    if submitter is not None:
        submitter.wait_until_completed()
    assert_tables_have_expected_len(
        database_reader,
        yield_number_of_rows_for_all_tables,
        expected_run_number=3,
        restarted=True,
    )
    # NOTE: The test in tests.unit.bout_runners.runner.test_bout_runner is testing
    #       restart_all=True, whether this is testing restart_from_bout_inp_dst=True
    assert_dump_files_exist(
        dump_dir_parent.joinpath(f"{dump_dir_name}_restart_0"))
    # ...and yet another entry
    submitter = runner.run_bout_run(bout_run_setup,
                                    restart_from_bout_inp_dst=True)
    if submitter is not None:
        submitter.wait_until_completed()
    assert_tables_have_expected_len(
        database_reader,
        yield_number_of_rows_for_all_tables,
        expected_run_number=4,
        restarted=True,
    )
    # NOTE: The test in tests.unit.bout_runners.runner.test_bout_runner is testing
    #       restart_all=True, whether this is testing restart_from_bout_inp_dst=True
    assert_dump_files_exist(
        dump_dir_parent.joinpath(f"{dump_dir_name}_restart_1"))
예제 #8
0
def test_run_bout_run(
    make_project: Path,
    get_bout_run_setup: Callable[[str], BoutRunSetup],
    yield_number_of_rows_for_all_tables: Callable[[DatabaseReader], Dict[str,
                                                                         int]],
    file_state_restorer: FileStateRestorer,
) -> None:
    """
    Test the BOUT++ run method.

    Parameters
    ----------
    make_project : Path
        The path to the conduction example
    get_bout_run_setup : function
        Function which returns the BoutRunSetup object based on the conduction directory
    yield_number_of_rows_for_all_tables : function
        Function which returns the number of rows for all tables in a schema
    file_state_restorer : FileStateRestorer
        Object for restoring files to original state
    """
    # Make project to save time
    _ = make_project

    run_graph = RunGraph()
    runner = BoutRunner(run_graph)

    bout_run_setup = get_bout_run_setup("test_run_bout_run")
    bout_paths = bout_run_setup.bout_paths
    db_connector = bout_run_setup.db_connector
    # NOTE: bout_run_setup.bout_paths.bout_inp_dst_dir will be removed in the
    #       yield_bout_path_conduction fixture (through the get_bout_run_setup
    #       fixture)
    #       Hence we do not need to add bout_run_setup.bout_paths.bout_inp_dst_dir
    #       to the file_state_restorer
    file_state_restorer.add(db_connector.db_path, force_mark_removal=True)

    # Run once
    submitter = bout_run_setup.submitter
    if runner.run_bout_run(bout_run_setup):
        submitter.wait_until_completed()
    # Assert that the run went well
    database_reader = assert_first_run(bout_paths, db_connector)
    # Assert that the number of runs is 1
    assert_tables_have_expected_len(database_reader,
                                    yield_number_of_rows_for_all_tables,
                                    expected_run_number=1)

    # Check that the run will not be executed again
    assert not runner.run_bout_run(bout_run_setup)
    # Assert that the number of runs is 1
    assert_tables_have_expected_len(database_reader,
                                    yield_number_of_rows_for_all_tables,
                                    expected_run_number=1)

    # Check that force overrides the behaviour
    if runner.run_bout_run(bout_run_setup, force=True):
        submitter.wait_until_completed()
    assert_tables_have_expected_len(database_reader,
                                    yield_number_of_rows_for_all_tables,
                                    expected_run_number=2)
    dump_dir_parent = bout_paths.bout_inp_dst_dir.parent
    dump_dir_name = bout_paths.bout_inp_dst_dir.name

    # Check that restart makes another entry
    bout_run_setup.executor.restart_from = bout_run_setup.bout_paths.bout_inp_dst_dir
    copy_restart_files(bout_run_setup.executor.restart_from,
                       bout_run_setup.bout_paths.bout_inp_dst_dir)
    if runner.run_bout_run(bout_run_setup):
        submitter.wait_until_completed()
    expected_run_number = 3
    assert_tables_have_expected_len(
        database_reader,
        yield_number_of_rows_for_all_tables,
        expected_run_number=expected_run_number,
        restarted=True,
    )
    # NOTE: The test in tests.unit.bout_runners.runner.test_bout_runner is testing
    #       restart_all=True, whether this is testing restart_from_bout_inp_dst=True
    assert_dump_files_exist(
        dump_dir_parent.joinpath(f"{dump_dir_name}_restart_0"))
    file_state_restorer.add(
        dump_dir_parent.joinpath(f"{dump_dir_name}_restart_0"),
        force_mark_removal=True)
    # ...and yet another entry
    bout_run_setup.executor.restart_from = bout_run_setup.bout_paths.bout_inp_dst_dir
    copy_restart_files(bout_run_setup.executor.restart_from,
                       bout_run_setup.bout_paths.bout_inp_dst_dir)
    if runner.run_bout_run(bout_run_setup):
        submitter.wait_until_completed()
    assert_tables_have_expected_len(
        database_reader,
        yield_number_of_rows_for_all_tables,
        expected_run_number=expected_run_number + 1,
        restarted=True,
    )
    # NOTE: The test in tests.unit.bout_runners.runner.test_bout_runner is testing
    #       restart_all=True, whether this is testing restart_from_bout_inp_dst=True
    assert_dump_files_exist(
        dump_dir_parent.joinpath(f"{dump_dir_name}_restart_1"))
    file_state_restorer.add(
        dump_dir_parent.joinpath(f"{dump_dir_name}_restart_1"),
        force_mark_removal=True)