-
Notifications
You must be signed in to change notification settings - Fork 0
Handle unreadable stdin in sanitize-string #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,10 @@ | |
|
|
||
| # pylint: disable=missing-module-docstring,fixme | ||
|
|
||
| import sys | ||
| from io import StringIO | ||
| from unittest import mock | ||
|
|
||
| from strip_markup.tests.strip_markup import TestStripMarkupBase | ||
| from stdisplay.tests.stdisplay import simple_escape_cases | ||
|
|
||
|
|
@@ -19,32 +23,167 @@ class TestSanitizeString(TestStripMarkupBase): | |
| maxDiff = None | ||
|
|
||
| argv0: str = "sanitize-string" | ||
| help_str: str = """\ | ||
| sanitize-string: Usage: sanitize-string [--help] max_length [string] | ||
| If no string is provided as an argument, the string is read from standard input. | ||
| Set max_length to 'nolimit' to allow arbitrarily long strings. | ||
| """ | ||
|
|
||
| def _test_args_with_exit( | ||
| self, | ||
| stdout_string: str, | ||
| stderr_string: str, | ||
| args: list[str], | ||
| exit_code: int, | ||
| ) -> None: | ||
| stdout_buf: StringIO = StringIO() | ||
| stderr_buf: StringIO = StringIO() | ||
| args_arr: list[str] = [self.argv0, *args] | ||
| with ( | ||
| mock.patch.object(sys, "argv", args_arr), | ||
| mock.patch.object(sys, "stdout", stdout_buf), | ||
| mock.patch.object(sys, "stderr", stderr_buf), | ||
| ): | ||
| result: int = sanitize_string_main() | ||
| self.assertEqual(stdout_buf.getvalue(), stdout_string) | ||
| self.assertEqual(stderr_buf.getvalue(), stderr_string) | ||
| self.assertEqual(result, exit_code) | ||
| stdout_buf.close() | ||
| stderr_buf.close() | ||
|
|
||
| def test_help(self) -> None: | ||
| """ | ||
| Ensure sanitize_string.py's help output is as expected. | ||
| """ | ||
|
|
||
| help_str: str = """\ | ||
| sanitize-string: Usage: sanitize-string [--help] max_length [string] | ||
| If no string is provided as an argument, the string is read from standard input. | ||
| Set max_length to 'nolimit' to allow arbitrarily long strings. | ||
| """ | ||
| self._test_args( | ||
| main_func=sanitize_string_main, | ||
| argv0=self.argv0, | ||
| stdout_string="", | ||
| stderr_string=help_str, | ||
| stderr_string=self.help_str, | ||
| args=["--help"], | ||
| ) | ||
| self._test_args( | ||
| main_func=sanitize_string_main, | ||
| argv0=self.argv0, | ||
| stdout_string="", | ||
| stderr_string=help_str, | ||
| stderr_string=self.help_str, | ||
| args=["-h"], | ||
| ) | ||
|
|
||
| def test_usage_errors(self) -> None: | ||
| """Ensure argument validation errors emit usage and exit non-zero.""" | ||
|
|
||
| error_cases: list[list[str]] = [ | ||
| [], | ||
| ["-5"], | ||
| ["not-a-number"], | ||
| ["1", "2", "3"], | ||
| ] | ||
|
|
||
| for args in error_cases: | ||
| self._test_args_with_exit( | ||
| stdout_string="", | ||
| stderr_string=self.help_str, | ||
| args=args, | ||
| exit_code=1, | ||
| ) | ||
|
|
||
| def test_missing_stdin(self) -> None: | ||
| """Verify sanitize_string exits cleanly when stdin is unavailable.""" | ||
|
|
||
| stdout_buf: StringIO = StringIO() | ||
| stderr_buf: StringIO = StringIO() | ||
| args_arr: list[str] = [self.argv0, "nolimit"] | ||
| with ( | ||
| mock.patch.object(sys, "argv", args_arr), | ||
| mock.patch.object(sys, "stdin", None), | ||
| mock.patch.object(sys, "stdout", stdout_buf), | ||
| mock.patch.object(sys, "stderr", stderr_buf), | ||
| ): | ||
| exit_code: int = sanitize_string_main() | ||
| self.assertEqual(stdout_buf.getvalue(), "") | ||
| self.assertEqual(stderr_buf.getvalue(), "") | ||
| self.assertEqual(exit_code, 0) | ||
| stdout_buf.close() | ||
| stderr_buf.close() | ||
|
Comment on lines
+92
to
+109
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We already test this in |
||
|
|
||
| def test_stdin_without_reconfigure(self) -> None: | ||
| """Ensure sanitize_string tolerates stdin objects lacking reconfigure.""" | ||
|
|
||
| stdout_buf: StringIO = StringIO() | ||
| stderr_buf: StringIO = StringIO() | ||
| stdin_buf: StringIO = StringIO("Sample input") | ||
| args_arr: list[str] = [self.argv0, "nolimit"] | ||
| original_pytest_module = sys.modules.pop("pytest", None) | ||
| try: | ||
| with ( | ||
| mock.patch.object(sys, "argv", args_arr), | ||
| mock.patch.object(sys, "stdin", stdin_buf), | ||
| mock.patch.object(sys, "stdout", stdout_buf), | ||
| mock.patch.object(sys, "stderr", stderr_buf), | ||
| ): | ||
| exit_code: int = sanitize_string_main() | ||
| finally: | ||
| if original_pytest_module is not None: | ||
| sys.modules["pytest"] = original_pytest_module | ||
| self.assertEqual(stdout_buf.getvalue(), "Sample input") | ||
| self.assertEqual(stderr_buf.getvalue(), "") | ||
| self.assertEqual(exit_code, 0) | ||
| stdout_buf.close() | ||
| stderr_buf.close() | ||
|
Comment on lines
+111
to
+134
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this actually useful? In real-world use stdin will either have What would be better is if the test suite actually provided a TextIOWrapper to stdin so that we could always use |
||
|
|
||
| def test_unreadable_stdin_raises_error(self) -> None: | ||
| """Ensure unreadable stdin streams fail gracefully.""" | ||
|
|
||
| class ExplodingStdin: | ||
| def read(self) -> str: # pragma: no cover - invoked via main | ||
| raise ValueError("boom") | ||
|
|
||
| stdout_buf: StringIO = StringIO() | ||
| stderr_buf: StringIO = StringIO() | ||
| args_arr: list[str] = [self.argv0, "nolimit"] | ||
| with ( | ||
| mock.patch.object(sys, "argv", args_arr), | ||
| mock.patch.object(sys, "stdin", ExplodingStdin()), | ||
| mock.patch.object(sys, "stdout", stdout_buf), | ||
| mock.patch.object(sys, "stderr", stderr_buf), | ||
| ): | ||
| exit_code: int = sanitize_string_main() | ||
| self.assertEqual(stdout_buf.getvalue(), "") | ||
| self.assertEqual( | ||
| stderr_buf.getvalue(), | ||
| "sanitize-string: failed to read from standard input\n", | ||
| ) | ||
| self.assertEqual(exit_code, 1) | ||
| stdout_buf.close() | ||
| stderr_buf.close() | ||
|
Comment on lines
+136
to
+160
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If stdin raises an error on read, something is very, very wrong most likely in the kernel. Reading from stdin should never fail, it should In that case exiting gracefully would actually be a bad thing. |
||
|
|
||
| def test_missing_read_attribute_returns_error(self) -> None: | ||
| """Validate stdin objects without read cause a clean failure.""" | ||
|
|
||
| class NoReadStdin: # pragma: no cover - exercised indirectly | ||
| pass | ||
|
|
||
| stdout_buf: StringIO = StringIO() | ||
| stderr_buf: StringIO = StringIO() | ||
| args_arr: list[str] = [self.argv0, "nolimit"] | ||
| with ( | ||
| mock.patch.object(sys, "argv", args_arr), | ||
| mock.patch.object(sys, "stdin", NoReadStdin()), | ||
| mock.patch.object(sys, "stdout", stdout_buf), | ||
| mock.patch.object(sys, "stderr", stderr_buf), | ||
| ): | ||
| exit_code: int = sanitize_string_main() | ||
| self.assertEqual(stdout_buf.getvalue(), "") | ||
| self.assertEqual( | ||
| stderr_buf.getvalue(), | ||
| "sanitize-string: standard input is unreadable\n", | ||
| ) | ||
| self.assertEqual(exit_code, 1) | ||
| stdout_buf.close() | ||
| stderr_buf.close() | ||
|
Comment on lines
+162
to
+185
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Again, this can only ever happen in tests, at least if sanitize_string is used properly. This test is unnecessary. |
||
|
|
||
| def test_safe_strings(self) -> None: | ||
| """ | ||
| Wrapper for _test_safe_strings (from TestStripMarkup) specific to | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is again guarding against conditions that must not occur in real-world use, so this isn't useful.
As described above though, the
if "pytest" not in sys.modulesbit can probably be lost if we patch stdin with an actual TextIOWrapper.