Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions entangled/io/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,15 @@ class Write(WriterBase):
def conflict(self, fs: AbstractFileCache, db: FileDB) -> Conflict | None:
if self.target not in fs:
return None
# If the file on disk matches the filedb record, it is exactly what
# Entangled wrote last time and was not edited outside of Entangled, so
# it is safe to overwrite. Comparing against the filedb is more reliable
# than comparing modification times against the current set of sources:
# the latter gives false positives when the dependency graph changes,
# e.g. when a target no longer depends on one of its former sources
# (see issue #96).
if fs[self.target].stat != db[self.target]:
return Conflict(self.target, "changed outside the control of Entangled")
if self.sources:
if all(fs[s].stat < fs[self.target].stat for s in self.sources):
return Conflict(self.target, "newer than all of its sources: " + ", ".join(
f"`{s}`" for s in set(self.sources)))
return None

@override
Expand Down
40 changes: 40 additions & 0 deletions test/io/test_transaction.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from contextlib import chdir
from pathlib import Path
from time import sleep

from entangled.io.transaction import Transaction, Create, Write, Delete
from entangled.io.filedb import filedb
Expand Down Expand Up @@ -46,3 +47,42 @@ def test_transaction(tmp_path: Path):
assert isinstance(t.actions[1], Delete)
t.run()
assert not Path("b").exists()


def test_changing_dependencies(tmp_path: Path):
"""Regression test for issue #96: when a target no longer depends on one of
its former sources, Entangled should still be able to update it. The target
is then necessarily newer than its remaining sources, but as long as it was
not edited outside of Entangled (i.e. it matches the filedb), overwriting it
is safe and should not be reported as a conflict."""
with chdir(tmp_path):
fs = FileCache()

# the source files are older than anything generated from them
with open("input1", "w") as f:
_ = f.write("source one")
with open("input2", "w") as f:
_ = f.write("source two")
sleep(0.01)

# first run: `output` is generated from both inputs
with filedb(fs=fs) as db:
t = Transaction(db)
t.write(Path("output"), "from one and two",
[Path("input1"), Path("input2")])
assert isinstance(t.actions[0], Create)
t.run()
assert Path("output").exists()

# `output` is now newer than its sources; `input2` no longer writes to
# it, so the content changes while only `input1` remains as a source
fs.reset()
with filedb(fs=fs) as db:
t = Transaction(db)
t.write(Path("output"), "from one only", [Path("input1")])
assert isinstance(t.actions[0], Write)
# `output` was not touched outside of Entangled, so this is fine
assert t.all_ok()
t.run()

assert "from one only" in Path("output").read_text()
Loading