diff --git a/README.md b/README.md index 0f9e49e..d9043ae 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,12 @@ To test: pytest ``` +Generate a coverage report with: + +``` +pytest --cov-report term-missing --cov yaml2ics +``` + [black](https://github.com/psf/black) and other linters are used to auto-format files (and enforced by CI). To install the git hooks, use `pre-commit install`. To run the tests/auto-formatting manually, use `pre-commit run --all-files`. diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..529b0a4 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,47 @@ +import os +import sys + +import pytest + +from yaml2ics import event_from_yaml, main + +basedir = os.path.abspath(os.path.join(os.path.dirname(__file__))) +example_calendar = os.path.join(basedir, "../example/test_calendar.yaml") + + +def test_cli(monkeypatch): + with monkeypatch.context() as m: + m.setattr(sys, "argv", ["yaml2ics.py", example_calendar]) + main() + + with monkeypatch.context() as m: + m.setattr(sys, "argv", ["yaml2ics.py"]) + + with pytest.raises(RuntimeError) as e: + main() + assert "Usage:" in str(e) + + with monkeypatch.context() as m: + m.setattr(sys, "argv", ["yaml2ics.py", "syzygy.yaml"]) + + with pytest.raises(RuntimeError) as e: + main() + assert "is not a file" in str(e) + + +def test_errors(): + with pytest.raises(RuntimeError) as e: + event_from_yaml({"repeat": {"interval": {}}}) + assert "interval must specify" in str(e) + + with pytest.raises(RuntimeError) as e: + event_from_yaml({"ics": "123"}) + assert "Invalid custom ICS" in str(e) + + with pytest.raises(RuntimeError) as e: + event_from_yaml({"repeat": {"interval": {"weeks": 1}}}) + assert "must specify end date for repeating events" in str(e) + + with pytest.raises(RuntimeError) as e: + event_from_yaml({"repeat": {"interval": {"epochs": 4}}}) + assert "expected interval to be specified in seconds, minutes" in str(e) diff --git a/yaml2ics.py b/yaml2ics.py index 77ad7f3..df0cdef 100644 --- a/yaml2ics.py +++ b/yaml2ics.py @@ -75,25 +75,20 @@ def event_from_yaml(event_yaml: dict, tz: datetime.tzinfo = None) -> ics.Event: interval = repeat["interval"] if not len(interval) == 1: - print( + raise RuntimeError( "Error: interval must specify seconds, minutes, hours, days, " - "weeks, months, or years only", - file=sys.stderr, + "weeks, months, or years only" ) - sys.exit(-1) interval_measure = list(interval.keys())[0] if interval_measure not in interval_type: - print( + raise RuntimeError( "Error: expected interval to be specified in seconds, minutes, " "hours, days, weeks, months, or years only", - file=sys.stderr, ) - sys.exit(-1) if "until" not in repeat: - print("Error: must specify end date for repeating events", file=sys.stderr) - sys.exit(-1) + raise RuntimeError("Error: must specify end date for " "repeating events") # This causes zero-length events, I guess overriding whatever duration # might have been specified @@ -131,6 +126,8 @@ def event_from_yaml(event_yaml: dict, tz: datetime.tzinfo = None) -> ics.Event: if ics_custom: for line in ics_custom.split("\n"): + if not line: + continue if ":" not in line: raise RuntimeError( f"Invalid custom ICS (expected `fieldname:value`):\n {line}" @@ -191,19 +188,21 @@ def files_to_calendar(files: list) -> ics.Calendar: def main(): if len(sys.argv) < 2: - print("Usage: yaml2ics.py FILE1.yaml FILE2.yaml ...") - sys.exit(-1) + raise RuntimeError("Usage: yaml2ics.py FILE1.yaml FILE2.yaml ...") files = sys.argv[1:] for f in files: if not os.path.isfile(f): - print(f"Error: {f} is not a file", file=sys.stderr) - sys.exit(-1) + raise RuntimeError(f"Error: {f} is not a file") calendar = files_to_calendar(files) print(calendar.serialize()) -if __name__ == "__main__": - main() +if __name__ == "__main__": # pragma: no cover + try: + main() + except Exception as e: + print(e, file=sys.stderr) + sys.exit(-1)