Coverage for pytest_beehave/bootstrap.py: 100%

40 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2026-04-21 04:49 +0000

1"""Bootstrap logic for the features directory structure.""" 

2 

3from __future__ import annotations 

4 

5from dataclasses import dataclass 

6from pathlib import Path 

7 

8_CANONICAL_SUBFOLDERS: tuple[str, ...] = ("backlog", "in-progress", "completed") 

9 

10 

11@dataclass(frozen=True, slots=True) 

12class BootstrapResult: 

13 """Result of bootstrapping the features directory. 

14 

15 Attributes: 

16 created_subfolders: Names of subfolders that were created. 

17 migrated_files: Paths of files that were migrated. 

18 collision_warnings: Warning messages for name collisions. 

19 """ 

20 

21 created_subfolders: tuple[str, ...] 

22 migrated_files: tuple[Path, ...] 

23 collision_warnings: tuple[str, ...] 

24 

25 @property 

26 def is_noop(self) -> bool: 

27 """Return True if bootstrap made no changes.""" 

28 return ( 

29 len(self.created_subfolders) == 0 

30 and len(self.migrated_files) == 0 

31 and len(self.collision_warnings) == 0 

32 ) 

33 

34 

35def _ensure_canonical_subfolders(features_root: Path) -> tuple[str, ...]: 

36 """Create missing canonical subfolders under features_root. 

37 

38 Args: 

39 features_root: The features root directory. 

40 

41 Returns: 

42 Tuple of subfolder names that were created. 

43 """ 

44 created: list[str] = [] 

45 for name in _CANONICAL_SUBFOLDERS: 

46 subfolder = features_root / name 

47 if not subfolder.exists(): 

48 subfolder.mkdir(parents=True, exist_ok=True) 

49 created.append(name) 

50 return tuple(created) 

51 

52 

53def _migrate_loose_feature_files( 

54 features_root: Path, 

55) -> tuple[tuple[Path, ...], tuple[str, ...]]: 

56 """Move any loose .feature files in features_root into backlog/. 

57 

58 Files that collide with an existing path in backlog/ are skipped 

59 with a warning. 

60 

61 Args: 

62 features_root: The features root directory. 

63 

64 Returns: 

65 Tuple of (migrated_paths, warning_strings). 

66 """ 

67 migrated: list[Path] = [] 

68 warnings: list[str] = [] 

69 backlog_dir = features_root / "backlog" 

70 for item in sorted(features_root.iterdir()): 

71 if item.is_dir() or item.suffix != ".feature": 

72 continue 

73 target = backlog_dir / item.name 

74 if target.exists(): 

75 warnings.append(f"Cannot migrate {item}: {target} already exists") 

76 continue 

77 item.rename(target) 

78 migrated.append(target) 

79 return tuple(migrated), tuple(warnings) 

80 

81 

82def bootstrap_features_directory(features_root: Path) -> BootstrapResult: 

83 """Ensure the features directory has the canonical subfolder structure. 

84 

85 Creates backlog/, in-progress/, and completed/ if missing. Migrates 

86 any loose .feature files at the root level into backlog/. 

87 

88 Args: 

89 features_root: Root of the features directory. 

90 

91 Returns: 

92 BootstrapResult describing what was done. 

93 """ 

94 if not features_root.exists(): 

95 return BootstrapResult( 

96 created_subfolders=(), 

97 migrated_files=(), 

98 collision_warnings=(), 

99 ) 

100 created = _ensure_canonical_subfolders(features_root) 

101 migrated, warnings = _migrate_loose_feature_files(features_root) 

102 return BootstrapResult( 

103 created_subfolders=created, 

104 migrated_files=migrated, 

105 collision_warnings=warnings, 

106 )