mirror of
https://github.com/Stone-Red-Code/dvc.git
synced 2026-11-02 07:18:25 +01:00
repro: introduce --keep-going/--ignore to continue on error (#9712)
* repro: introduce --keep-going/--ignore to continue on error `-k`/`--keep-going` will skip all dependents of a stage if it fails. It'll still try to reproduce the other dependencies of the pending targets, before it exits with a non-zero status. `--ignore-errors` will treat any error during stage execution as a success, and will continue executing other dependencies as if it succeeded. Exits with 0 even on failure. Also fixes: * --force-downstream used to force everything even if one upstream nodes changed. This used to work when we supported a single targets, but there could be independent pipelines, so we should not force everything if any node changes. Now we'll force reproduce the stage only if one of its dependency changed upstream or it's upstream node was also forced. * Makes `--downstream` more correct, in relation to `--allow-missing` and `--pull`. Now we use an active graph for downstream/upstream logic instead of a subgraph. Note that this does not affect the downstream logic though, as dvc always goes downstream, so subgraph was no different than active graph. The subgraph only did not have the upstream nodes. * add tests * add spacing in the message
This commit is contained in:
@@ -943,6 +943,18 @@ def test_repro_force_downstream(tmp_dir, dvc, copy_script):
|
||||
assert stages[1].addressing == file3_stage.addressing
|
||||
|
||||
|
||||
def test_repro_force_downstream_do_not_force_independent_stages(tmp_dir, dvc, run_copy):
|
||||
tmp_dir.gen({"foo": "foo", "bar": "bar"})
|
||||
foo1 = run_copy("foo", "foo1", name="foo1")
|
||||
foo2 = run_copy("foo1", "foo2", name="foo2")
|
||||
run_copy("bar", "bar1", name="bar1")
|
||||
run_copy("bar1", "bar2", name="bar2")
|
||||
cat = dvc.run(cmd="cat bar2 foo2", deps=["foo2", "bar2"], name="cat")
|
||||
|
||||
tmp_dir.gen("foo", "foobar")
|
||||
assert dvc.reproduce(force_downstream=True) == [foo1, foo2, cat]
|
||||
|
||||
|
||||
def test_repro_pipeline(
|
||||
tmp_dir,
|
||||
dvc,
|
||||
@@ -1296,3 +1308,53 @@ def test_repro_single_item_with_multiple_targets(tmp_dir, dvc, copy_script):
|
||||
stage2,
|
||||
stage1,
|
||||
]
|
||||
|
||||
|
||||
def test_repro_keep_going(mocker, tmp_dir, dvc, copy_script):
|
||||
from dvc.repo import reproduce
|
||||
|
||||
(bar_stage, foo_stage) = tmp_dir.dvc_gen({"bar": "bar", "foo": "foo"})
|
||||
stage1 = dvc.stage.add(
|
||||
cmd=["python copy.py bar foobar", "exit 1"],
|
||||
deps=["bar"],
|
||||
outs=["foobar"],
|
||||
name="copy-bar-foobar",
|
||||
)
|
||||
dvc.stage.add(cmd="cat foobar foo", deps=["foobar", "foo"], name="cat")
|
||||
spy = mocker.spy(reproduce, "_reproduce_stage")
|
||||
|
||||
with pytest.raises(ReproductionError):
|
||||
dvc.reproduce(on_error="keep-going", repro_fn=spy)
|
||||
|
||||
assert spy.call_args_list == [
|
||||
mocker.call(bar_stage, upstream=[], force=False, interactive=False),
|
||||
mocker.call(stage1, upstream=[bar_stage], force=False, interactive=False),
|
||||
mocker.call(foo_stage, upstream=[], force=False, interactive=False),
|
||||
]
|
||||
|
||||
|
||||
def test_repro_ignore_errors(M, mocker, tmp_dir, dvc, copy_script):
|
||||
from dvc.repo import reproduce
|
||||
|
||||
(bar_stage, foo_stage) = tmp_dir.dvc_gen({"bar": "bar", "foo": "foo"})
|
||||
stage1 = dvc.stage.add(
|
||||
cmd=["python copy.py bar foobar", "exit 1"],
|
||||
deps=["bar"],
|
||||
outs=["foobar"],
|
||||
name="copy-bar-foobar",
|
||||
)
|
||||
stage2 = dvc.stage.add(cmd="cat foobar foo", deps=["foobar", "foo"], name="cat")
|
||||
spy = mocker.spy(reproduce, "_reproduce_stage")
|
||||
dvc.reproduce(on_error="ignore", repro_fn=spy)
|
||||
|
||||
assert spy.call_args_list == [
|
||||
mocker.call(bar_stage, upstream=[], force=False, interactive=False),
|
||||
mocker.call(stage1, upstream=[bar_stage], force=False, interactive=False),
|
||||
mocker.call(foo_stage, upstream=[], force=False, interactive=False),
|
||||
mocker.call(
|
||||
stage2,
|
||||
upstream=[foo_stage, stage1],
|
||||
force=False,
|
||||
interactive=False,
|
||||
),
|
||||
]
|
||||
|
||||
@@ -14,6 +14,7 @@ common_arguments = {
|
||||
"pull": False,
|
||||
"allow_missing": False,
|
||||
"targets": [],
|
||||
"on_error": "fail",
|
||||
}
|
||||
repro_arguments = {
|
||||
"run_cache": True,
|
||||
|
||||
@@ -1,45 +1,12 @@
|
||||
from itertools import chain
|
||||
|
||||
from networkx import DiGraph
|
||||
from networkx.utils import graphs_equal
|
||||
|
||||
from dvc.repo.reproduce import plan_repro
|
||||
from dvc.repo.reproduce import get_active_graph, plan_repro
|
||||
|
||||
|
||||
def test_number_reproduces(tmp_dir, dvc, mocker):
|
||||
reproduce_stage_mock = mocker.patch(
|
||||
"dvc.repo.reproduce._reproduce_stage", returns=[]
|
||||
)
|
||||
tmp_dir.dvc_gen({"pre-foo": "pre-foo"})
|
||||
|
||||
dvc.run(name="echo-foo", outs=["foo"], cmd="echo foo > foo")
|
||||
dvc.run(name="echo-bar", deps=["foo"], outs=["bar"], cmd="echo bar > bar")
|
||||
dvc.run(name="echo-baz", deps=["foo"], outs=["baz"], cmd="echo baz > baz")
|
||||
dvc.run(name="echo-boop", deps=["bar"], outs=["boop"], cmd="echo boop > boop")
|
||||
|
||||
reproduce_stage_mock.reset_mock()
|
||||
|
||||
dvc.reproduce(all_pipelines=True)
|
||||
|
||||
assert reproduce_stage_mock.call_count == 5
|
||||
|
||||
|
||||
def test_repro_plan(mocker):
|
||||
r"""
|
||||
n1
|
||||
/ \
|
||||
n2 m3 n8
|
||||
/ \ / \ |
|
||||
n4 n5 n6 n7 n9
|
||||
"""
|
||||
|
||||
# note: downstream steps may not be stable, use AnyOf in such cases
|
||||
class AnyOf:
|
||||
def __init__(self, *items):
|
||||
self.items = items
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return any(item == other for item in self.items)
|
||||
|
||||
def test_active_graph(mocker):
|
||||
n = mocker.sentinel
|
||||
n1, n2, n3, n4, n5, n6, n7, n8, n9 = (getattr(n, f"n{i}") for i in range(1, 10))
|
||||
edges = {n1: [n2, n3], n2: [n4, n5], n3: [n6, n7], n8: [n9]}
|
||||
@@ -47,24 +14,58 @@ def test_repro_plan(mocker):
|
||||
node.frozen = False
|
||||
|
||||
g = DiGraph(edges)
|
||||
assert plan_repro(g) == [n4, n5, n2, n6, n7, n3, n1, n9, n8]
|
||||
assert plan_repro(g, [n1]) == [n4, n5, n2, n6, n7, n3, n1]
|
||||
assert plan_repro(g, [n4], downstream=True) == [n4, n2, n1]
|
||||
assert plan_repro(g, [n8], True) == plan_repro(g, [n9], True) == [n9, n8]
|
||||
assert plan_repro(g, [n2, n8], True) == [n4, n5, n2, n6, n7, n3, n1, n9, n8]
|
||||
assert plan_repro(g, [n2, n3], downstream=True) == [
|
||||
AnyOf(n2, n3),
|
||||
AnyOf(n2, n3),
|
||||
n1,
|
||||
]
|
||||
|
||||
active = get_active_graph(g)
|
||||
assert graphs_equal(g, active)
|
||||
|
||||
n2.frozen = True
|
||||
assert plan_repro(g) == [n2, n6, n7, n3, n1, n9, n8, n4, n5]
|
||||
assert plan_repro(g, [n1]) == [n2, n6, n7, n3, n1]
|
||||
assert plan_repro(g, [n4], downstream=True) == [n4]
|
||||
assert plan_repro(g, [n2, n8], pipeline=True) == [n2, n6, n7, n3, n1, n9, n8]
|
||||
assert plan_repro(g, [n2, n3], downstream=True) == [
|
||||
AnyOf(n2, n3),
|
||||
AnyOf(n2, n3),
|
||||
n1,
|
||||
active = get_active_graph(g)
|
||||
assert g.edges() - active.edges() == {(n2, n5), (n2, n4)}
|
||||
assert n2 in active
|
||||
assert not active.edges() - g.edges()
|
||||
assert not graphs_equal(g, active)
|
||||
|
||||
|
||||
def test_repro_plan(M):
|
||||
r"""
|
||||
1
|
||||
/ \
|
||||
2 3 8
|
||||
/ \ / \ |
|
||||
4 5 6 7 9
|
||||
"""
|
||||
g = DiGraph({1: [2, 3], 2: [4, 5], 3: [6, 7], 8: [9]})
|
||||
|
||||
assert plan_repro(g) == [4, 5, 2, 6, 7, 3, 1, 9, 8]
|
||||
assert plan_repro(g, [1]) == [4, 5, 2, 6, 7, 3, 1]
|
||||
assert plan_repro(g, [4], downstream=True) == [4, 2, 1]
|
||||
assert plan_repro(g, [8], True) == plan_repro(g, [9], True) == [9, 8]
|
||||
assert plan_repro(g, [2, 8], True) == [4, 5, 2, 6, 7, 3, 1, 9, 8]
|
||||
assert plan_repro(g, [2, 3], downstream=True) == [
|
||||
M.any_of(2, 3),
|
||||
M.any_of(2, 3),
|
||||
1,
|
||||
]
|
||||
|
||||
|
||||
def test_number_reproduces(tmp_dir, dvc, mocker):
|
||||
mock = mocker.Mock(return_value=None)
|
||||
tmp_dir.dvc_gen({"pre-foo": "pre-foo"})
|
||||
|
||||
dvc.stage.add(name="echo-foo", outs=["foo"], cmd="echo foo > foo", verify=False)
|
||||
dvc.stage.add(
|
||||
name="echo-bar", deps=["foo"], outs=["bar"], cmd="echo bar > bar", verify=False
|
||||
)
|
||||
dvc.stage.add(
|
||||
name="echo-baz", deps=["foo"], outs=["baz"], cmd="echo baz > baz", verify=False
|
||||
)
|
||||
dvc.stage.add(
|
||||
name="echo-boop",
|
||||
deps=["bar"],
|
||||
outs=["boop"],
|
||||
cmd="echo boop > boop",
|
||||
verify=False,
|
||||
)
|
||||
|
||||
dvc.reproduce(all_pipelines=True, repro_fn=mock)
|
||||
assert mock.call_count == 5
|
||||
|
||||
Reference in New Issue
Block a user