dotx 3.2.1__tar.gz → 3.2.2__tar.gz
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.
- {dotx-3.2.1/src/dotx.egg-info → dotx-3.2.2}/PKG-INFO +1 -1
- {dotx-3.2.1 → dotx-3.2.2}/pyproject.toml +1 -1
- {dotx-3.2.1 → dotx-3.2.2}/src/dotx/commands/database.py +36 -6
- {dotx-3.2.1 → dotx-3.2.2}/src/dotx/commands/install_cmd.py +10 -2
- {dotx-3.2.1 → dotx-3.2.2}/src/dotx/database.py +53 -14
- {dotx-3.2.1 → dotx-3.2.2}/src/dotx/install.py +17 -2
- {dotx-3.2.1 → dotx-3.2.2/src/dotx.egg-info}/PKG-INFO +1 -1
- {dotx-3.2.1 → dotx-3.2.2}/tests/test_cli.py +4 -2
- {dotx-3.2.1 → dotx-3.2.2}/tests/test_cli_database.py +48 -2
- {dotx-3.2.1 → dotx-3.2.2}/tests/test_install.py +53 -0
- {dotx-3.2.1 → dotx-3.2.2}/LICENSE +0 -0
- {dotx-3.2.1 → dotx-3.2.2}/MANIFEST.in +0 -0
- {dotx-3.2.1 → dotx-3.2.2}/README.md +0 -0
- {dotx-3.2.1 → dotx-3.2.2}/setup.cfg +0 -0
- {dotx-3.2.1 → dotx-3.2.2}/src/dotx/__init__.py +0 -0
- {dotx-3.2.1 → dotx-3.2.2}/src/dotx/always-create +0 -0
- {dotx-3.2.1 → dotx-3.2.2}/src/dotx/cli.py +0 -0
- {dotx-3.2.1 → dotx-3.2.2}/src/dotx/commands/__init__.py +0 -0
- {dotx-3.2.1 → dotx-3.2.2}/src/dotx/commands/path_cmd.py +0 -0
- {dotx-3.2.1 → dotx-3.2.2}/src/dotx/commands/progress.py +0 -0
- {dotx-3.2.1 → dotx-3.2.2}/src/dotx/commands/uninstall_cmd.py +0 -0
- {dotx-3.2.1 → dotx-3.2.2}/src/dotx/dotxignore +0 -0
- {dotx-3.2.1 → dotx-3.2.2}/src/dotx/hierarchy.py +0 -0
- {dotx-3.2.1 → dotx-3.2.2}/src/dotx/ignore.py +0 -0
- {dotx-3.2.1 → dotx-3.2.2}/src/dotx/installed-schema.sql +0 -0
- {dotx-3.2.1 → dotx-3.2.2}/src/dotx/options.py +0 -0
- {dotx-3.2.1 → dotx-3.2.2}/src/dotx/plan.py +0 -0
- {dotx-3.2.1 → dotx-3.2.2}/src/dotx/uninstall.py +0 -0
- {dotx-3.2.1 → dotx-3.2.2}/src/dotx.egg-info/SOURCES.txt +0 -0
- {dotx-3.2.1 → dotx-3.2.2}/src/dotx.egg-info/dependency_links.txt +0 -0
- {dotx-3.2.1 → dotx-3.2.2}/src/dotx.egg-info/entry_points.txt +0 -0
- {dotx-3.2.1 → dotx-3.2.2}/src/dotx.egg-info/requires.txt +0 -0
- {dotx-3.2.1 → dotx-3.2.2}/src/dotx.egg-info/top_level.txt +0 -0
- {dotx-3.2.1 → dotx-3.2.2}/tests/test_always_create.py +0 -0
- {dotx-3.2.1 → dotx-3.2.2}/tests/test_ignore.py +0 -0
- {dotx-3.2.1 → dotx-3.2.2}/tests/test_ignore_rules.py +0 -0
- {dotx-3.2.1 → dotx-3.2.2}/tests/test_options.py +0 -0
- {dotx-3.2.1 → dotx-3.2.2}/tests/test_path_which.py +0 -0
- {dotx-3.2.1 → dotx-3.2.2}/tests/test_plan.py +0 -0
- {dotx-3.2.1 → dotx-3.2.2}/tests/test_uninstall.py +0 -0
|
@@ -152,9 +152,13 @@ def register_commands(app: typer.Typer):
|
|
|
152
152
|
if issues:
|
|
153
153
|
console.print(f"\n[bold cyan]{pkg_name}:[/bold cyan]")
|
|
154
154
|
for issue in issues:
|
|
155
|
-
console.print(f" [red]✗[/red] {issue[
|
|
156
|
-
console.print(f" [dim]Issue: {issue[
|
|
157
|
-
console.print(f" [dim]Expected: {issue[
|
|
155
|
+
console.print(f" [red]✗[/red] {issue['target_path']}")
|
|
156
|
+
console.print(f" [dim]Issue: {issue['issue']}[/dim]")
|
|
157
|
+
console.print(f" [dim]Expected type: {issue['link_type']}[/dim]")
|
|
158
|
+
if "expected" in issue:
|
|
159
|
+
console.print(f" [dim]Expected source: {issue['expected']}[/dim]")
|
|
160
|
+
if "actual" in issue:
|
|
161
|
+
console.print(f" [dim]Actual target: {issue['actual']}[/dim]")
|
|
158
162
|
total_issues += len(issues)
|
|
159
163
|
|
|
160
164
|
if total_issues == 0:
|
|
@@ -409,6 +413,7 @@ def register_commands(app: typer.Typer):
|
|
|
409
413
|
with InstallationDB() as db:
|
|
410
414
|
all_packages = db.get_all_packages()
|
|
411
415
|
total_would_clean = 0
|
|
416
|
+
total_dangling_symlinks = 0
|
|
412
417
|
|
|
413
418
|
for pkg_info in all_packages:
|
|
414
419
|
pkg_root = Path(pkg_info["package_root"])
|
|
@@ -416,10 +421,16 @@ def register_commands(app: typer.Typer):
|
|
|
416
421
|
orphaned = db.get_orphaned_entries(pkg_root, pkg_name)
|
|
417
422
|
if orphaned:
|
|
418
423
|
total_would_clean += len(orphaned)
|
|
424
|
+
# Count dangling symlinks that would be removed
|
|
425
|
+
dangling = [e for e in orphaned if Path(e["target_path"]).is_symlink()]
|
|
426
|
+
total_dangling_symlinks += len(dangling)
|
|
419
427
|
console.print(f" {pkg_name}: {len(orphaned)} orphaned entry(ies)")
|
|
428
|
+
if dangling and verbose:
|
|
429
|
+
for entry in dangling:
|
|
430
|
+
console.print(f" [dim]Would remove: {entry['target_path']}[/dim]")
|
|
420
431
|
|
|
421
432
|
if total_would_clean > 0:
|
|
422
|
-
console.print(f"[yellow]Would remove {total_would_clean} orphaned entry(ies).[/yellow]")
|
|
433
|
+
console.print(f"[yellow]Would remove {total_would_clean} orphaned DB entry(ies) and {total_dangling_symlinks} dangling symlink(s).[/yellow]")
|
|
423
434
|
else:
|
|
424
435
|
console.print("[green]No orphaned entries to clean.[/green]")
|
|
425
436
|
|
|
@@ -462,18 +473,37 @@ def register_commands(app: typer.Typer):
|
|
|
462
473
|
console.print("\n[cyan]Cleaning orphaned entries...[/cyan]")
|
|
463
474
|
all_packages = db.get_all_packages()
|
|
464
475
|
total_cleaned = 0
|
|
476
|
+
total_symlinks_removed = 0
|
|
465
477
|
|
|
466
478
|
for pkg_info in all_packages:
|
|
467
479
|
pkg_root = Path(pkg_info["package_root"])
|
|
468
480
|
pkg_name = pkg_info["package_name"]
|
|
481
|
+
|
|
482
|
+
# Get orphaned entries before cleaning to remove dangling symlinks
|
|
483
|
+
orphaned = db.get_orphaned_entries(pkg_root, pkg_name)
|
|
484
|
+
|
|
485
|
+
# Remove dangling symlinks from filesystem
|
|
486
|
+
for entry in orphaned:
|
|
487
|
+
target_path = Path(entry["target_path"])
|
|
488
|
+
if target_path.is_symlink():
|
|
489
|
+
try:
|
|
490
|
+
target_path.unlink()
|
|
491
|
+
total_symlinks_removed += 1
|
|
492
|
+
logger.info(f"Removed dangling symlink: {target_path}")
|
|
493
|
+
if verbose:
|
|
494
|
+
console.print(f" [dim]Removed: {target_path}[/dim]")
|
|
495
|
+
except OSError as e:
|
|
496
|
+
logger.warning(f"Failed to remove symlink {target_path}: {e}")
|
|
497
|
+
|
|
498
|
+
# Clean database entries
|
|
469
499
|
cleaned = db.clean_orphaned_entries(pkg_root, pkg_name)
|
|
470
500
|
if cleaned > 0:
|
|
471
501
|
total_cleaned += cleaned
|
|
472
502
|
if verbose:
|
|
473
503
|
console.print(f" Cleaned {cleaned} orphaned entry(ies) from {pkg_name}")
|
|
474
504
|
|
|
475
|
-
if total_cleaned > 0:
|
|
476
|
-
console.print(f"[green]✓ Removed {total_cleaned} orphaned entry(ies).[/green]")
|
|
505
|
+
if total_cleaned > 0 or total_symlinks_removed > 0:
|
|
506
|
+
console.print(f"[green]✓ Removed {total_cleaned} orphaned DB entry(ies) and {total_symlinks_removed} dangling symlink(s).[/green]")
|
|
477
507
|
else:
|
|
478
508
|
console.print("[green]✓ No orphaned entries found.[/green]")
|
|
479
509
|
|
|
@@ -56,10 +56,18 @@ def register_command(app: typer.Typer):
|
|
|
56
56
|
if failures:
|
|
57
57
|
can_install = False
|
|
58
58
|
console.print(
|
|
59
|
-
f"[red]✗ Error: can't install {source_package.name} -
|
|
59
|
+
f"[red]✗ Error: can't install {source_package.name} - conflicts detected:[/red]"
|
|
60
60
|
)
|
|
61
61
|
for plan_node in failures:
|
|
62
|
-
|
|
62
|
+
dest_path = target_path / plan_node.relative_destination_path
|
|
63
|
+
if dest_path.is_symlink():
|
|
64
|
+
try:
|
|
65
|
+
link_target = dest_path.readlink()
|
|
66
|
+
console.print(f" {dest_path} [dim](symlink → {link_target})[/dim]")
|
|
67
|
+
except OSError:
|
|
68
|
+
console.print(f" {dest_path} [dim](broken symlink)[/dim]")
|
|
69
|
+
else:
|
|
70
|
+
console.print(f" {dest_path} [dim](existing file)[/dim]")
|
|
63
71
|
console.print()
|
|
64
72
|
|
|
65
73
|
if can_install:
|
|
@@ -315,12 +315,43 @@ class InstallationDB:
|
|
|
315
315
|
|
|
316
316
|
return [dict(row) for row in cursor.fetchall()]
|
|
317
317
|
|
|
318
|
+
def _verify_symlink(self, target: Path, source_package_root: Path) -> dict | None:
|
|
319
|
+
"""
|
|
320
|
+
Verify a symlink points to the correct source.
|
|
321
|
+
|
|
322
|
+
Returns an issue dict if there's a problem, None if symlink is correct.
|
|
323
|
+
"""
|
|
324
|
+
try:
|
|
325
|
+
link_target = target.readlink()
|
|
326
|
+
actual_target = target.resolve()
|
|
327
|
+
|
|
328
|
+
# Check if it's a broken symlink (target doesn't exist)
|
|
329
|
+
if not actual_target.exists():
|
|
330
|
+
return {
|
|
331
|
+
"issue": "Broken symlink (target missing)",
|
|
332
|
+
"actual": str(link_target),
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
# Check if symlink points under source_package_root
|
|
336
|
+
try:
|
|
337
|
+
actual_target.relative_to(source_package_root)
|
|
338
|
+
return None # Symlink is correct
|
|
339
|
+
except ValueError:
|
|
340
|
+
return {
|
|
341
|
+
"issue": "Symlink points to wrong target",
|
|
342
|
+
"expected": str(source_package_root),
|
|
343
|
+
"actual": str(link_target),
|
|
344
|
+
}
|
|
345
|
+
except OSError:
|
|
346
|
+
return {"issue": "Cannot read symlink target"}
|
|
347
|
+
|
|
318
348
|
def verify_installations(self, package_root: Path, package_name: str) -> list[dict]:
|
|
319
349
|
"""
|
|
320
350
|
Verify installations for a package against filesystem.
|
|
321
351
|
|
|
322
352
|
Checks if database records match actual filesystem state. Each issue
|
|
323
|
-
is a dict with keys: target_path, issue (description), and
|
|
353
|
+
is a dict with keys: target_path, issue (description), link_type, and
|
|
354
|
+
optionally expected/actual for symlink target mismatches.
|
|
324
355
|
"""
|
|
325
356
|
if not self.conn:
|
|
326
357
|
raise RuntimeError("Database not connected")
|
|
@@ -331,29 +362,37 @@ class InstallationDB:
|
|
|
331
362
|
for install in installations:
|
|
332
363
|
target = Path(install["target_path"])
|
|
333
364
|
link_type = install["link_type"]
|
|
365
|
+
source_package_root = Path(install["source_package_root"])
|
|
334
366
|
|
|
335
|
-
# Check if file exists
|
|
336
|
-
if not target.exists():
|
|
337
|
-
issues.append({
|
|
338
|
-
"target_path": str(target),
|
|
339
|
-
"issue": "File missing (in DB but not on filesystem)",
|
|
340
|
-
"link_type": link_type,
|
|
341
|
-
})
|
|
342
|
-
continue
|
|
343
|
-
|
|
344
|
-
# Check if it's a symlink (for file and directory types)
|
|
345
367
|
if link_type in ("file", "directory"):
|
|
346
368
|
if not target.is_symlink():
|
|
369
|
+
if target.exists():
|
|
370
|
+
issue = "Not a symlink (regular file/dir exists instead)"
|
|
371
|
+
else:
|
|
372
|
+
issue = "Missing (not on filesystem)"
|
|
347
373
|
issues.append({
|
|
348
374
|
"target_path": str(target),
|
|
349
|
-
"issue":
|
|
375
|
+
"issue": issue,
|
|
350
376
|
"link_type": link_type,
|
|
351
377
|
})
|
|
378
|
+
else:
|
|
379
|
+
symlink_issue = self._verify_symlink(target, source_package_root)
|
|
380
|
+
if symlink_issue:
|
|
381
|
+
symlink_issue["target_path"] = str(target)
|
|
382
|
+
symlink_issue["link_type"] = link_type
|
|
383
|
+
issues.append(symlink_issue)
|
|
384
|
+
|
|
352
385
|
elif link_type == "created_dir":
|
|
353
|
-
if not target.
|
|
386
|
+
if not target.exists():
|
|
387
|
+
issue = "Missing (not on filesystem)"
|
|
388
|
+
elif not target.is_dir() or target.is_symlink():
|
|
389
|
+
issue = "Not a regular directory (should be created dir)"
|
|
390
|
+
else:
|
|
391
|
+
issue = None
|
|
392
|
+
if issue:
|
|
354
393
|
issues.append({
|
|
355
394
|
"target_path": str(target),
|
|
356
|
-
"issue":
|
|
395
|
+
"issue": issue,
|
|
357
396
|
"link_type": link_type,
|
|
358
397
|
})
|
|
359
398
|
|
|
@@ -147,12 +147,27 @@ def plan_install(source_package_root: Path, destination_root: Path) -> Plan:
|
|
|
147
147
|
# we can't link the whole directory - must CREATE it instead
|
|
148
148
|
if plan[child_relative_source_path].requires_rename:
|
|
149
149
|
found_children_to_rename = True
|
|
150
|
-
# Fail if we would overwrite an existing
|
|
150
|
+
# Fail if we would overwrite an existing file or symlink pointing elsewhere
|
|
151
151
|
destination_path = (
|
|
152
152
|
destination_root
|
|
153
153
|
/ plan[child_relative_source_path].relative_destination_path
|
|
154
154
|
)
|
|
155
|
-
if destination_path.
|
|
155
|
+
if destination_path.is_symlink():
|
|
156
|
+
# Symlink exists - only OK if it already points to our source
|
|
157
|
+
expected_source = (
|
|
158
|
+
source_package_root / plan[child_relative_source_path].relative_source_path
|
|
159
|
+
).resolve()
|
|
160
|
+
try:
|
|
161
|
+
actual_target = destination_path.resolve()
|
|
162
|
+
except OSError:
|
|
163
|
+
# Broken symlink - treat as conflict
|
|
164
|
+
actual_target = None
|
|
165
|
+
if actual_target == expected_source:
|
|
166
|
+
# Already installed correctly - skip it
|
|
167
|
+
plan[child_relative_source_path].action = Action.SKIP
|
|
168
|
+
else:
|
|
169
|
+
plan[child_relative_source_path].action = Action.FAIL
|
|
170
|
+
elif destination_path.exists() and destination_path.is_file():
|
|
156
171
|
plan[child_relative_source_path].action = Action.FAIL
|
|
157
172
|
|
|
158
173
|
# Second pass: decide action for current directory based on children and destination state
|
|
@@ -42,9 +42,9 @@ def test_cli_install_conflict_detection(tmp_path):
|
|
|
42
42
|
|
|
43
43
|
assert result.exit_code == 0 # CLI doesn't return error code, just refuses
|
|
44
44
|
assert "can't install" in result.output.lower()
|
|
45
|
-
assert "
|
|
45
|
+
assert "conflicts detected" in result.output.lower()
|
|
46
|
+
assert "existing file" in result.output.lower()
|
|
46
47
|
assert "Refusing to install" in result.output
|
|
47
|
-
assert "conflicts detected" in result.output
|
|
48
48
|
|
|
49
49
|
|
|
50
50
|
def test_cli_install_with_verbose(tmp_path, isolated_db):
|
|
@@ -295,5 +295,7 @@ def test_cli_uninstall_dry_run_shows_indicator(tmp_path, isolated_db):
|
|
|
295
295
|
# Should clearly indicate dry-run mode
|
|
296
296
|
assert "[DRY RUN]" in result.output
|
|
297
297
|
assert "Would remove" in result.output
|
|
298
|
+
# Should show the rm command that would be executed
|
|
299
|
+
assert "rm" in result.output
|
|
298
300
|
# File should still exist after dry-run
|
|
299
301
|
assert (target / "file1").exists()
|
|
@@ -117,6 +117,52 @@ def test_verify_not_symlink(tmp_path):
|
|
|
117
117
|
assert "not a symlink" in issues[0]["issue"].lower()
|
|
118
118
|
|
|
119
119
|
|
|
120
|
+
def test_verify_wrong_symlink_target(tmp_path):
|
|
121
|
+
"""Test 'dotx verify' detects symlinks pointing to wrong target."""
|
|
122
|
+
db_path = tmp_path / "test.db"
|
|
123
|
+
package = tmp_path / "package"
|
|
124
|
+
package.mkdir()
|
|
125
|
+
(package / "dot-bashrc").write_text("# correct source")
|
|
126
|
+
|
|
127
|
+
target_file = tmp_path / ".bashrc"
|
|
128
|
+
# Create symlink pointing to wrong location
|
|
129
|
+
wrong_target = tmp_path / "wrong-file"
|
|
130
|
+
wrong_target.write_text("# wrong target")
|
|
131
|
+
target_file.symlink_to(wrong_target)
|
|
132
|
+
|
|
133
|
+
# Record as if it were pointing to package
|
|
134
|
+
with InstallationDB(db_path) as db:
|
|
135
|
+
db.record_installation(tmp_path, "package", package, target_file, "file")
|
|
136
|
+
|
|
137
|
+
# Verify - should detect wrong target
|
|
138
|
+
with InstallationDB(db_path) as db:
|
|
139
|
+
issues = db.verify_installations(tmp_path, "package")
|
|
140
|
+
assert len(issues) == 1
|
|
141
|
+
assert "wrong target" in issues[0]["issue"].lower()
|
|
142
|
+
assert "actual" in issues[0]
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_verify_broken_symlink(tmp_path):
|
|
146
|
+
"""Test 'dotx verify' detects broken symlinks."""
|
|
147
|
+
db_path = tmp_path / "test.db"
|
|
148
|
+
package = tmp_path / "package"
|
|
149
|
+
package.mkdir()
|
|
150
|
+
|
|
151
|
+
target_file = tmp_path / ".bashrc"
|
|
152
|
+
# Create symlink pointing to nonexistent file
|
|
153
|
+
target_file.symlink_to("/nonexistent/path")
|
|
154
|
+
|
|
155
|
+
# Record installation
|
|
156
|
+
with InstallationDB(db_path) as db:
|
|
157
|
+
db.record_installation(tmp_path, "package", package, target_file, "file")
|
|
158
|
+
|
|
159
|
+
# Verify - should detect broken symlink
|
|
160
|
+
with InstallationDB(db_path) as db:
|
|
161
|
+
issues = db.verify_installations(tmp_path, "package")
|
|
162
|
+
assert len(issues) == 1
|
|
163
|
+
assert "broken" in issues[0]["issue"].lower()
|
|
164
|
+
|
|
165
|
+
|
|
120
166
|
def test_show_package(tmp_path):
|
|
121
167
|
"""Test 'dotx show' displays package installation details."""
|
|
122
168
|
db_path = tmp_path / "test.db"
|
|
@@ -557,7 +603,7 @@ def test_cli_sync_clean_dry_run(tmp_path, monkeypatch):
|
|
|
557
603
|
|
|
558
604
|
assert result.exit_code == 0
|
|
559
605
|
assert "Would clean orphaned entries" in result.output
|
|
560
|
-
assert "Would remove 1 orphaned entry(ies)" in result.output
|
|
606
|
+
assert "Would remove 1 orphaned DB entry(ies)" in result.output
|
|
561
607
|
assert "Dry run" in result.output
|
|
562
608
|
|
|
563
609
|
|
|
@@ -604,7 +650,7 @@ def test_cli_sync_clean(tmp_path, monkeypatch):
|
|
|
604
650
|
|
|
605
651
|
assert result.exit_code == 0
|
|
606
652
|
assert "Cleaning orphaned entries" in result.output
|
|
607
|
-
assert "Removed 1 orphaned entry(ies)" in result.output
|
|
653
|
+
assert "Removed 1 orphaned DB entry(ies)" in result.output
|
|
608
654
|
|
|
609
655
|
# Check database now has only 1 entry
|
|
610
656
|
with InstallationDB() as db:
|
|
@@ -289,6 +289,59 @@ def test_install_normal_file_fail(tmp_path):
|
|
|
289
289
|
)
|
|
290
290
|
|
|
291
291
|
|
|
292
|
+
def test_install_symlink_to_wrong_target_fails(tmp_path):
|
|
293
|
+
"""Test that existing symlink pointing to wrong target is marked FAIL."""
|
|
294
|
+
source_package_root = tmp_path / "source"
|
|
295
|
+
destination_root = tmp_path / "dest"
|
|
296
|
+
source_package_root.mkdir()
|
|
297
|
+
destination_root.mkdir()
|
|
298
|
+
file_path = Path("file1")
|
|
299
|
+
(source_package_root / file_path).write_text("content")
|
|
300
|
+
|
|
301
|
+
# Create a symlink pointing to a different location
|
|
302
|
+
wrong_target = tmp_path / "wrong_target"
|
|
303
|
+
wrong_target.write_text("wrong content")
|
|
304
|
+
(destination_root / file_path).symlink_to(wrong_target)
|
|
305
|
+
|
|
306
|
+
plan = plan_install(source_package_root, destination_root)
|
|
307
|
+
|
|
308
|
+
assert plan[file_path].action == Action.FAIL
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def test_install_broken_symlink_fails(tmp_path):
|
|
312
|
+
"""Test that existing broken symlink is marked FAIL."""
|
|
313
|
+
source_package_root = tmp_path / "source"
|
|
314
|
+
destination_root = tmp_path / "dest"
|
|
315
|
+
source_package_root.mkdir()
|
|
316
|
+
destination_root.mkdir()
|
|
317
|
+
file_path = Path("file1")
|
|
318
|
+
(source_package_root / file_path).write_text("content")
|
|
319
|
+
|
|
320
|
+
# Create a broken symlink (target doesn't exist)
|
|
321
|
+
(destination_root / file_path).symlink_to("/nonexistent/path")
|
|
322
|
+
|
|
323
|
+
plan = plan_install(source_package_root, destination_root)
|
|
324
|
+
|
|
325
|
+
assert plan[file_path].action == Action.FAIL
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def test_install_symlink_to_our_source_skips(tmp_path):
|
|
329
|
+
"""Test that existing symlink pointing to our source is marked SKIP."""
|
|
330
|
+
source_package_root = tmp_path / "source"
|
|
331
|
+
destination_root = tmp_path / "dest"
|
|
332
|
+
source_package_root.mkdir()
|
|
333
|
+
destination_root.mkdir()
|
|
334
|
+
file_path = Path("file1")
|
|
335
|
+
(source_package_root / file_path).write_text("content")
|
|
336
|
+
|
|
337
|
+
# Create a symlink pointing to our source (already installed)
|
|
338
|
+
(destination_root / file_path).symlink_to(source_package_root / file_path)
|
|
339
|
+
|
|
340
|
+
plan = plan_install(source_package_root, destination_root)
|
|
341
|
+
|
|
342
|
+
assert plan[file_path].action == Action.SKIP
|
|
343
|
+
|
|
344
|
+
|
|
292
345
|
def test_install_hidden_file(tmp_path):
|
|
293
346
|
source_package_root = tmp_path / "source"
|
|
294
347
|
destination_root = tmp_path / "dest"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|