agent-bios 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -94,18 +94,29 @@ def prune(state_dir, homes, now=None, dry=False):
94
94
  now = time.time() if now is None else now
95
95
  removed = []
96
96
 
97
+ # Recorded AFTER the removal is confirmed gone. Both deleters swallow their errors on
98
+ # purpose — a backup that will not delete must not fail an install — but appending
99
+ # first made the returned list a record of what was ATTEMPTED, and the caller prints it
100
+ # as what was removed. A copy still on disk being reported as pruned is the reading
101
+ # that sends someone looking for space that was never freed.
102
+ def take(path, remove):
103
+ if dry:
104
+ removed.append(path)
105
+ return
106
+ remove(path)
107
+ if path.exists():
108
+ print(f" warning: could not remove {path}", file=sys.stderr)
109
+ return
110
+ removed.append(path)
111
+
97
112
  backups = state_dir / "backups"
98
113
  if backups.is_dir():
99
114
  for path in to_delete(_entries([d for d in backups.iterdir() if d.is_dir()]), now):
100
- removed.append(path)
101
- if not dry:
102
- shutil.rmtree(path, ignore_errors=True)
115
+ take(path, lambda p: shutil.rmtree(p, ignore_errors=True))
103
116
 
104
117
  for home in homes:
105
118
  for path in to_delete(_entries(owned_siblings(home)), now):
106
- removed.append(path)
107
- if not dry:
108
- path.unlink(missing_ok=True)
119
+ take(path, lambda p: p.unlink(missing_ok=True))
109
120
  return removed
110
121
 
111
122
 
@@ -163,7 +174,46 @@ def self_test():
163
174
  if not missed and not grabbed:
164
175
  print(f" ok ownership: claims {len(ours)} shapes we write, "
165
176
  f"refuses {len(theirs)} we do not")
166
- print(f"prune-backups self-test: {'OK' if not bad else 'FAIL'} ({len(cases) + 2} checks)")
177
+ # The reported list is what the caller prints as removed, so it has to be what is GONE.
178
+ # Both deleters swallow their errors by design — a stuck copy must not fail an install —
179
+ # and appending before the call made the list a record of attempts instead. Driven on a
180
+ # real directory, because the defect lives in the deletion and not in the pure retention
181
+ # rule above; the deletable run beside it is what keeps this from passing on a pruner
182
+ # that reports nothing at all.
183
+ import os as os_, tempfile as tempfile_
184
+
185
+ def reported_vs_gone(block_one):
186
+ root = pathlib.Path(tempfile_.mkdtemp())
187
+ state = root / "state"
188
+ (state / "backups").mkdir(parents=True)
189
+ made = []
190
+ for i in range(KEEP_RECENT + 2):
191
+ aged = state / "backups" / f"2020{i:04d}-000000"
192
+ aged.mkdir()
193
+ (aged / "f.txt").write_text("x")
194
+ stamp = now - (MAX_AGE_DAYS + 10 + i) * day
195
+ os_.utime(aged, (stamp, stamp))
196
+ made.append(aged)
197
+ stuck = sorted(made, key=lambda q: q.stat().st_mtime)[0]
198
+ if block_one:
199
+ stuck.chmod(0o500)
200
+ count = len(prune(state, [], now=now))
201
+ gone = len([q for q in made if not q.exists()])
202
+ if block_one:
203
+ stuck.chmod(0o700)
204
+ shutil.rmtree(root, ignore_errors=True)
205
+ return count, gone
206
+
207
+ for label, block_one in (("every aged backup deletable", False),
208
+ ("one of them undeletable", True)):
209
+ count, gone = reported_vs_gone(block_one)
210
+ if count != gone or (not block_one and gone == 0):
211
+ print(f" FAIL {label}: reported {count} removed, {gone} actually gone")
212
+ bad += 1
213
+ else:
214
+ print(f" ok {label}: reported {count} == gone {gone}")
215
+
216
+ print(f"prune-backups self-test: {'OK' if not bad else 'FAIL'} ({len(cases) + 4} checks)")
167
217
  return 1 if bad else 0
168
218
 
169
219