-
Notifications
You must be signed in to change notification settings - Fork 17
Add test coverage for entrypoint #25
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
Open
AbcSxyZ
wants to merge
4
commits into
dogecoin:main
Choose a base branch
from
AbcSxyZ:test-coverage
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| """ | ||
| Pytest configuration file for tests fixtures, global variables | ||
| and error messages for tests errors. | ||
|
|
||
| Pytest fixtures are used to arrange and clean up tests environment. | ||
| See: https://docs.pytest.org/en/6.2.x/fixture.html | ||
| """ | ||
|
|
||
| import os | ||
| import tempfile | ||
| import pytest | ||
| from hooks.entrypoint_hook import EntrypointHook | ||
| from hooks.command import Command | ||
|
|
||
| def abs_path(executable): | ||
| """Format expected location of dogecoin executables in containers""" | ||
| return os.path.join(pytest.executables_folder, executable) | ||
|
|
||
| def pytest_configure(): | ||
| """Declare global variables to use across tests""" | ||
| # User used for tests | ||
| pytest.user = os.environ["USER"] | ||
|
|
||
| # Perform tests in a temporary directory, used as datadir | ||
| pytest.directory = tempfile.TemporaryDirectory() | ||
| pytest.datadir = pytest.directory.name | ||
|
|
||
| # Location where dogecoin executables should be located | ||
| pytest.executables_folder = "/usr/local/bin" | ||
| pytest.abs_path = abs_path | ||
|
|
||
| @pytest.fixture | ||
| def hook(): | ||
| """ | ||
| Prepare & cleanup EntrypointHook for tests, by disabling and restoring | ||
| entrypoint functions & system calls. | ||
|
|
||
| EntrypointHook.test is then used inside a test, available as hook.test. | ||
| """ | ||
| test_hook = EntrypointHook() | ||
| yield test_hook | ||
| test_hook.reset_hooks() | ||
|
|
||
| def pytest_assertrepr_compare(left, right): | ||
| """Override error messages of AssertionError on test failure.""" | ||
| # Display comparison of result command and an expected execve command | ||
| if isinstance(left, Command) and isinstance(right, Command): | ||
| assert_msg = ["fail"] | ||
| assert_msg.append("======= Result =======") | ||
| assert_msg.extend(str(left).splitlines()) | ||
| assert_msg.append("======= Expected =======") | ||
| assert_msg.extend(str(right).splitlines()) | ||
| assert_msg.append("======= Diff =======") | ||
| assert_msg.extend(left.diff(right)) | ||
| return assert_msg | ||
| return None |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| """ | ||
| Command interface for os.execve hook from EntrypointHook. | ||
|
|
||
| Store arguments and environ for hooked commands, used to create | ||
| both the command of entrypoint.py called during tests, | ||
| and the expected command for test comparison. | ||
| """ | ||
|
|
||
| import difflib | ||
| import json | ||
|
|
||
| class CommandNotFound(Exception): | ||
| """Raised when entrypoint command is not found or test fail.""" | ||
|
|
||
| class Command: | ||
| """ | ||
| Represent a single execve command, with | ||
| its arguments and environment. | ||
|
|
||
| Can represent an entrypoint hooked command, the expected | ||
| result or the input of a test. | ||
| """ | ||
| def __init__(self, argv, environ): | ||
| self.argv = argv | ||
| self.environ = environ | ||
|
|
||
| def __eq__(self, other): | ||
| """Compare 2 Command, result of a test and expected command.""" | ||
| return self.argv == other.argv and self.environ == other.environ | ||
|
|
||
| def __str__(self): | ||
| """Render single command into string for error outputs.""" | ||
| argv_str = json.dumps(self.argv, indent=4) | ||
| command_str = f"argv: {argv_str}\n" | ||
| environ_str = json.dumps(self.environ, indent=4) | ||
| command_str += f"environ: {environ_str}" | ||
| return command_str | ||
|
|
||
| def diff(self, other): | ||
| """Perform diff between result command and expected command.""" | ||
| command = str(self).splitlines() | ||
| other_command = str(other).splitlines() | ||
|
|
||
| return difflib.unified_diff(command, other_command, | ||
| fromfile="result", tofile="expected", lineterm="") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,155 @@ | ||
| """ | ||
| Hook for tests of entrypoint.py behavior. Abstract functions and system | ||
| call to catch arguments used by entrypoint, or disable unwanted functions. | ||
|
|
||
| EntrypointHook.test is used to perform a single test by | ||
| calling entrypoint.main. The hook is available as a test fixture, inside | ||
| a test argument. | ||
|
|
||
| Test example: | ||
|
|
||
| def test_for_entrypoint(hook): | ||
| # Values to test | ||
| test_argv = [value1, ...] | ||
| test_env = {key:value, ...} | ||
|
|
||
| # Expected result | ||
| result_argv = [value1, ...] | ||
| result_env = {key:value, ...} | ||
|
|
||
| # Perform the test using the hook | ||
| hook.test(test_argv, test_env, result_argv, result_env) | ||
| assert hook.result == hook.reference | ||
|
|
||
| Visit also pytest doc for test formatting: https://docs.pytest.org/ | ||
| """ | ||
|
|
||
| import sys | ||
| import os | ||
| import shutil | ||
| import entrypoint | ||
| import hooks.help_menus | ||
| from hooks.command import Command, CommandNotFound | ||
|
|
||
| class EntrypointHook: | ||
| """ | ||
| Hook to perform tests of the Dockerfile entrypoint.py. Manage all | ||
| informations about test result and expected output for test comparison. | ||
|
|
||
| Hook some system calls & functions used by `entrypoint.main` to handle | ||
| commands which should have been run by the script. | ||
| Disable some function related so file permissions & creation. | ||
|
|
||
| See `self._setup_hooks` for all defined hooks. | ||
| """ | ||
| # Environment to use for every tests Command & comparison Command | ||
| DEFAULT_ENV = { | ||
| "USER" : os.environ["USER"], | ||
| "PATH" : os.environ["PATH"], | ||
| } | ||
|
|
||
| def __init__(self): | ||
| self.result = None | ||
| self.reference = None | ||
|
|
||
| self._setup_hooks() | ||
|
|
||
| def test(self, test_argv, test_environ, \ | ||
| result_argv, result_environ): | ||
| """ | ||
| Run a test of entrypoint.main and store expected result in the hook | ||
| for further comparaison. | ||
|
|
||
| - self.result store entrypoint.py command launched by main | ||
| - self.reference store the expected Command for comparison. | ||
|
|
||
| Stored Command objects are comparable, used for asserts. | ||
| Example: | ||
| >>> assert hook.result == hook.reference | ||
| """ | ||
| # Clean hook from previous test, store the command to test | ||
| self._reset_attributes() | ||
|
|
||
| # Default environment variables needed by all tests | ||
| test_environ.update(self.DEFAULT_ENV) | ||
| result_environ.update(self.DEFAULT_ENV) | ||
|
|
||
| # Manage system arguments & environment used by the script | ||
| sys.argv[1:] = test_argv.copy() | ||
| os.environ = test_environ.copy() | ||
|
|
||
| # Run the test, launching entrypoint script from the main | ||
| entrypoint.main() | ||
|
|
||
| # Store expected Command used for comparison | ||
| self.reference = Command(result_argv, result_environ) | ||
|
|
||
| if self.result is None: | ||
| raise CommandNotFound("Test fail, do not return a command") | ||
|
|
||
| def _execve_hook(self, executable, argv, environ): | ||
| """ | ||
| Hook for os.execve function, to catch arguments/environment | ||
| instead of launching processes. | ||
| """ | ||
| assert executable == argv[0] | ||
| self.result = Command(argv, environ) | ||
|
|
||
| @staticmethod | ||
| def _get_help_hook(command_arguments): | ||
| """ | ||
| Hook call of executable help menu to retrieve options. | ||
| Fake a list of raw options generated by entrypoint.get_help. | ||
| """ | ||
| executable = command_arguments[0] | ||
|
|
||
| #Test use of -help-debug to expand help options | ||
| if executable == "dogecoind": | ||
| assert "-help-debug" in command_arguments | ||
| else: | ||
| assert "-help-debug" not in command_arguments | ||
| return getattr(hooks.help_menus, executable.replace("-", "_")) | ||
|
|
||
| def _reset_attributes(self): | ||
| """Clean state between each test""" | ||
| self.result = None | ||
| self.reference = None | ||
|
|
||
| def _setup_hooks(self): | ||
| """ | ||
| Enable hooks of entrypoint.py system & functions calls, disable | ||
| some functions. | ||
|
|
||
| Replace entrypoint function by EntrypointHook methods | ||
| to handle arguments used by entrypoint calls. | ||
|
|
||
| Save references to previous functions to restore them test | ||
| clean up. | ||
| """ | ||
| # Save hooked functions for restoration | ||
| self._execve_backup = os.execve | ||
| self._setgid_backup = os.setgid | ||
| self._setuid_backup = os.setuid | ||
| self._which_backup = shutil.which | ||
| self._get_help_backup = entrypoint.get_help | ||
|
|
||
| # Setup hooks | ||
| # Add execve hook globally to catch entrypoint arguments | ||
| os.execve = self._execve_hook | ||
| # Hook executables call to `-help` menus to fake options | ||
| entrypoint.get_help = self._get_help_hook | ||
|
|
||
| # which not working from host without dogecoin executables in PATH | ||
| shutil.which = lambda executable : f"/usr/local/bin/{executable}" | ||
|
|
||
| # Disable setgid & setuid behavior | ||
| os.setgid = lambda _ : None | ||
| os.setuid = lambda _ : None | ||
|
|
||
| def reset_hooks(self): | ||
| """Restore hooks of `self._setup_hooks` to initial functions""" | ||
| os.execve = self._execve_backup | ||
| os.setgid = self._setgid_backup | ||
| os.setuid = self._setuid_backup | ||
| shutil.which = self._which_backup | ||
| entrypoint.get_help = self._get_help_backup |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.