--- /dev/null
+from typing import Optional, Dict, Iterator, Tuple, Set, Union
+from pathlib import PurePosixPath
+from os import PathLike
+from posixpath import normpath
+from enum import Enum
+import errno
+from functools import cached_property
+
+
+class MockDir(Enum):
+ DIR = ""
+
+
+DIR = MockDir.DIR
+
+
+class MockFilesystem:
+ files: Dict[str, Union[bytes, MockDir]]
+ current_dir: PurePosixPath
+
+ def __init__(self,
+ files: Optional[Dict[str, Union[bytes, MockDir]]] = None,
+ current_dir: Optional[PathLike] = None):
+ if files is None:
+ files = {"/": DIR}
+ self.files = files
+ if current_dir is None:
+ current_dir = PurePosixPath("/")
+ self.current_dir = current_dir
+
+ def normalize_path(self, path: PathLike) -> PurePosixPath:
+ abs_path = self.current_dir.joinpath(path)
+ return PurePosixPath(normpath(abs_path))
+
+ def check_parent(self, normalized_path: PurePosixPath):
+ parent = self.files.get(str(normalized_path.parent))
+ if parent is None:
+ raise FileNotFoundError(normalized_path)
+ if parent is not DIR:
+ raise NotADirectoryError(normalized_path)
+
+ def create(self, path: PathLike, contents: Union[bytes, MockDir]):
+ normalized_path = self.normalize_path(path)
+ self.check_parent(normalized_path)
+ if str(normalized_path) in self.files:
+ raise FileExistsError(normalized_path)
+ self.files[str(normalized_path)] = contents
+
+ def create_or_write_file(self, path: PathLike, contents: bytes):
+ normalized_path = self.normalize_path(path)
+ self.check_parent(normalized_path)
+ if self.files.get(str(normalized_path)) is DIR:
+ raise IsADirectoryError(normalized_path)
+ self.files[str(normalized_path)] = contents
+
+ def write_existing_file(self, path: PathLike, contents: bytes):
+ normalized_path = self.normalize_path(path)
+ self.check_parent(normalized_path)
+ old_file = self.files.get(str(normalized_path))
+ if old_file is None:
+ raise FileNotFoundError(normalized_path)
+ if old_file is DIR:
+ raise IsADirectoryError(normalized_path)
+ self.files[str(normalized_path)] = contents
+
+ def is_dir(self, path: PathLike) -> bool:
+ normalized_path = self.normalize_path(path)
+ return self.files.get(str(normalized_path)) is DIR
+
+ def change_dir(self, path: PathLike):
+ normalized_path = self.normalize_path(path)
+ f = self.files.get(str(normalized_path))
+ if f is None:
+ raise FileNotFoundError(normalized_path)
+ if f is not DIR:
+ raise NotADirectoryError(normalized_path)
+ self.current_dir = normalized_path
+
+ def __repr__(self) -> str:
+ return f"MockFilesystem(files={self.files!r}, " \
+ f"current_dir={self.current_dir!r})"
+
+
+class MockPath:
+ filesystem: MockFilesystem
+ path: PurePosixPath
+
+ def __init__(self, path: PathLike, filesystem: MockFilesystem):
+ self.path = PurePosixPath(path)
+ self.filesystem = filesystem
+
+ @cached_property
+ def parent(self) -> "MockPath":
+ return MockPath(self.path.parent, self.filesystem)
+
+ def __eq__(self, other) -> bool:
+ return self.path == other.path
+
+ def is_dir(self) -> bool:
+ self.filesystem.is_dir(self.path)
+
+ def mkdir(self, parents: bool = False, exist_ok: bool = False):
+ # derived from Python's Path.mkdir
+ try:
+ self.filesystem.create(self.path, DIR)
+ except FileNotFoundError:
+ if not parents or self.parent == self:
+ raise
+ self.parent.mkdir(parents=True, exist_ok=True)
+ self.mkdir(parents=False, exist_ok=exist_ok)
+ except OSError:
+ if not exist_ok or not self.is_dir():
+ raise
+
+ def joinpath(self, *args) -> "MockPath":
+ return MockPath(self.path.joinpath(*args), self.filesystem)
+
+ def write_bytes(self, data):
+ self.filesystem.create_or_write_file(self.path,
+ bytes(memoryview(data)))
+
+ def write_text(self, data, encoding=None, errors=None):
+ if not isinstance(data, str):
+ raise TypeError()
+ if encoding is None:
+ encoding = "utf-8"
+ if errors is None:
+ errors = "strict"
+ self.write_bytes(data.encode(encoding=encoding, errors=errors))
+
+ def __str__(self) -> str:
+ return str(self.path)
+
+ def __repr__(self) -> str:
+ return repr(self.path)
--- /dev/null
+import unittest
+from budget_sync.config import Config
+from budget_sync.test.mock_bug import MockBug
+from budget_sync.test.mock_path import MockPath, DIR
+from budget_sync.test.test_mock_path import make_filesystem_and_report_if_error
+from budget_sync.budget_graph import BudgetGraph
+from budget_sync.write_budget_markdown import write_budget_markdown
+
+
+class TestWriteBudgetMarkdown(unittest.TestCase):
+ def test(self):
+ config = Config.from_str(
+ """
+ bugzilla_url = "https://bugzilla.example.com/"
+ [milestones]
+ [people."person1"]
+ output_markdown_file = "person1.mdwn"
+ [people."person2"]
+ output_markdown_file = "person2.mdwn"
+ """)
+ budget_graph = BudgetGraph([
+ MockBug(bug_id=1,
+ cf_budget_parent=None,
+ cf_budget="0",
+ cf_total_budget="0",
+ cf_nlnet_milestone=None,
+ cf_payees_list="",
+ summary=""),
+ ], config)
+ self.assertEqual([], budget_graph.get_errors())
+ with make_filesystem_and_report_if_error(self) as filesystem:
+ output_dir = MockPath("/output_dir/", filesystem=filesystem)
+ write_budget_markdown(budget_graph, output_dir)
+ self.assertEqual({
+ "/": DIR,
+ "/output_dir": DIR,
+ '/output_dir/person1.mdwn': b'<!-- autogenerated by '
+ b'budget-sync -->\n# person1\n\n# Status Tracking\n',
+ '/output_dir/person2.mdwn': b'<!-- autogenerated by '
+ b'budget-sync -->\n# person2\n\n# Status Tracking\n',
+ }, filesystem.files)
+
+
+if __name__ == "__main__":
+ unittest.main()