diff --git a/Pipfile b/Pipfile index dea3505aa..79f9af4f2 100644 --- a/Pipfile +++ b/Pipfile @@ -6,6 +6,7 @@ name = "pypi" [packages] gitpython = "*" requests = ">=2.20.0" +pygithub = "*" [dev-packages] pylint = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 91b91c131..dde604baa 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "dfd95da88f8bd7bc96eb87c1ecc54a8f1bfee720243ab2f6eb2c50e71c98b11c" + "sha256": "2ce59710697fafac5eb5536b8fa7b930c91f31e6884e3b57914161ec07592133" }, "pipfile-spec": 6, "requires": {}, @@ -28,6 +28,13 @@ ], "version": "==3.0.4" }, + "deprecated": { + "hashes": [ + "sha256:2f293eb0eee34b1fcf3da530fe8fc4b0d71d43ddc2dc78e2ffb444b6c0868557", + "sha256:749f6cdcfbdc3f79258f8154bad43fced95adc632c337675d0385959895894bc" + ], + "version": "==1.2.5" + }, "gitdb2": { "hashes": [ "sha256:83361131a1836661a155172932a13c08bda2db3674e4caa32368aa6eb02f38c2", @@ -50,6 +57,20 @@ ], "version": "==2.8" }, + "pygithub": { + "hashes": [ + "sha256:263102b43a83e2943900c1313109db7a00b3b78aeeae2c9137ba694982864872" + ], + "index": "pypi", + "version": "==1.43.5" + }, + "pyjwt": { + "hashes": [ + "sha256:5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e", + "sha256:8d59a976fb773f3e6a39c85636357c4f0e242707394cadadd9814f5cbaa20e96" + ], + "version": "==1.7.1" + }, "requests": { "hashes": [ "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", @@ -71,6 +92,12 @@ "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" ], "version": "==1.24.1" + }, + "wrapt": { + "hashes": [ + "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" + ], + "version": "==1.11.1" } }, "develop": { @@ -104,11 +131,11 @@ }, "black": { "hashes": [ - "sha256:817243426042db1d36617910df579a54f1afd659adb96fc5032fcf4b36209739", - "sha256:e030a9a28f542debc08acceb273f228ac422798e5215ba2a791a6ddeaaca22a5" + "sha256:09a9dcb7c46ed496a9850b76e4e825d6049ecd38b611f1224857a79bd985a8cf", + "sha256:68950ffd4d9169716bcb8719a56c07a2f4485354fec061cdd5910aa07369731c" ], "index": "pypi", - "version": "==18.9b0" + "version": "==19.3b0" }, "certifi": { "hashes": [ diff --git a/gator/.DS_Store b/gator/.DS_Store new file mode 100644 index 000000000..6978dbfbf Binary files /dev/null and b/gator/.DS_Store differ diff --git a/gator/arguments.py b/gator/arguments.py index 2e6ee6f97..db62cb24d 100644 --- a/gator/arguments.py +++ b/gator/arguments.py @@ -29,6 +29,57 @@ def parse(args): # CORRECT WHEN: user provides this argument but not any other main arguments gg_parser.add_argument("--commits", type=int, help="minimum number of git commits") + # specify a check for the number of issues raised in the Github issue tracker + # CORRECT WHEN: user provides this argument along with a github token, the + # name of the repo to check and the name of the user to check + gg_parser.add_argument("--issues", type=int, help="minimum number of issues raised") + + # specify a check for the number of comments made on issues in the Github + # issue tracker + # CORRECT WHEN: user provides this argument along with a github token, the + # name of the repo to check and the name of the user to check + gg_parser.add_argument( + "--issue-comments", type=int, help="minimum number of comment made on issues" + ) + + # specify the github token to use for authenication + # CORRECT WHEN: user provides along with issues or issue comments, a repo + # name, and the user to check + gg_parser.add_argument( + "--token", + type=str, + metavar="TOKEN", + help="authenication token to access a github repository", + ) + + # specify the github repository to check the issues/comments of + # CORRECT WHEN: user provides along with issues or issue comments, a token, + # and the user to check + gg_parser.add_argument( + "--repo", + type=str, + metavar="REPO", + help="name of the repository to check the issues or comments of", + ) + + # specify the name of the user to check + # CORRECT WHEN: user provides along with issues or issue comments, a repo + # name, and a github token + gg_parser.add_argument( + "--name", + type=str, + metavar="NAME", + help="name of the creator of the issues or comments to check", + ) + + gg_parser.add_argument( + "--state", + type=str, + metavar="ISSUE_STATE", + default="all", + help="state of the issues to check, defaults to 'all'", + ) + # specify a single file and a single directory # CORRECT WHEN: user provides both of these gg_parser.add_argument( @@ -172,6 +223,48 @@ def is_valid_commits(args): return False +def is_valid_issues(args): + """Checks if it is a valid issues specification""" + if args.issues is not None: + return True + return False + + +def is_valid_issue_comments(args): + """Checks if it is a valid issue comment specification""" + if args.issue_comments is not None: + return True + return False + + +def is_valid_token(args): + """Checks if it is a valid token specification""" + if args.token is not None: + return True + return False + + +def is_valid_repo(args): + """Checks if it is a valid repo specification""" + if args.repo is not None: + return True + return False + + +def is_valid_name(args): + """Checks if it is a valid name specification""" + if args.name is not None: + return True + return False + + +def is_valid_state(args): + """Checks if it is a valid state""" + if args.state is not None: + return True + return False + + # }}} # Ancillary helper functions {{{ @@ -310,7 +403,8 @@ def verify(args): if is_valid_exists(args): # verified_arguments = True file_verified.append(True) - # VERIFIED: correct check for comments with language in a file in a directory + # VERIFIED: correct check for comments with language in a file in a + # directory if is_valid_comments(args) and is_valid_language(args): # verified_arguments = True file_verified.append(True) @@ -369,6 +463,13 @@ def verify(args): and not is_command_ancillary(args) ): verified_arguments = True + # no file or directory details were specified or a command given + # and the argumenet is a request to check the number of issues a person + # has made in the github issue tracker + elif (is_valid_issues(args) or is_valid_issue_comments(args)) and ( + is_valid_token(args) and is_valid_repo(args) and is_valid_name(args) + ): + verified_arguments = True return verified_arguments diff --git a/gator/invoke.py b/gator/invoke.py index 2590dbb75..42e184f41 100644 --- a/gator/invoke.py +++ b/gator/invoke.py @@ -8,6 +8,7 @@ from gator import repository from gator import run from gator import util +from gator import issues JAVA = "Java" PYTHON = "Python" @@ -51,6 +52,65 @@ def invoke_commits_check(student_repository, expected_count, exact=False): return did_check_pass +def invoke_issues_check(github_token, repo_name, username, expected_count, issue_state): + """Checks to see if a student has made any issues in the issue tracker""" + # gets the number of issues the user has made in the tracker + did_check_pass, num_issues, err = issues.check_issues_made( + github_token, repo_name, username, expected_count, issue_state + ) + message = ( + str(username) + " has at made at least " + str(expected_count) + " issue(s)" + ) + if err == -1: + diagnostic = "Invalid Github Token Supplied: '" + github_token + "'" + elif err == -2: + diagnostic = "Invalid Repository Supplied: '" + repo_name + "'" + else: + diagnostic = ( + "Found " + + str(num_issues) + + " issue(s)" + + " made by " + + str(username) + + " in the " + + repo_name + + " repository" + ) + update_report(did_check_pass, message, diagnostic) + return did_check_pass + + +# pylint: disable=bad-continuation +def invoke_issue_comments_check( + github_token, repo_name, username, expected_count, issue_state +): + """Checks to see if a student has made any comments on issues in the issue tracker""" + # gets the number of comments the user has made on issues in the tracker + did_check_pass, num_comments, err = issues.check_comments_made( + github_token, repo_name, username, expected_count, issue_state + ) + message = ( + str(username) + " has at made at least " + str(expected_count) + " comment(s)" + ) + if err == -1: + diagnostic = "Invalid Github Token Supplied: '" + github_token + "'" + elif err == -2: + diagnostic = "Invalid Repository Supplied: '" + repo_name + "'" + else: + diagnostic = ( + "Found " + + str(num_comments) + + " comment(s)" + + " made by " + + str(username) + + " in the " + + repo_name + + " repository" + ) + update_report(did_check_pass, message, diagnostic) + return did_check_pass + + def invoke_file_in_directory_check(filecheck, directory): """Check to see if the file is in the directory""" # get the home directory for checking and then check for file diff --git a/gator/issues.py b/gator/issues.py new file mode 100644 index 000000000..8917f0a82 --- /dev/null +++ b/gator/issues.py @@ -0,0 +1,45 @@ +"""Get issues from the Github issue tracker and performs checks on them""" + +from github import Github +from github.GithubException import UnknownObjectException, BadCredentialsException + + +def check_issues_made(token, repo, name, expected, issue_state): + """Returns the number of issues that the given user has made""" + # github access + g = Github(token) + # gets the repo + try: + repo = g.get_repo(repo) + except BadCredentialsException: + return False, 0, -1 + except UnknownObjectException: + return False, 0, -2 + issues_made = 0 + for issue in repo.get_issues(state=issue_state): + if issue.user.login == name and issue.pull_request is None: + issues_made += 1 + if issues_made >= expected: + return True, issues_made, 0 + return issues_made >= expected, issues_made, 0 + + +def check_comments_made(token, repo, name, expected, issue_state): + """Returns the number of comments that the given user has made""" + # github access + g = Github(token) + # gets the repo + try: + repo = g.get_repo(repo) + except BadCredentialsException: + return False, 0, -1 + except UnknownObjectException: + return False, 0, -2 + comments_made = 0 + for issue in repo.get_issues(state=issue_state): + for comment in issue.get_comments(): + if comment.user.login == name and issue.pull_request is None: + comments_made += 1 + if comments_made >= expected: + return True, comments_made, 0 + return comments_made >= expected, comments_made, 0 diff --git a/gator/orchestrate.py b/gator/orchestrate.py index 886cb7dcb..c599c54f3 100644 --- a/gator/orchestrate.py +++ b/gator/orchestrate.py @@ -69,6 +69,46 @@ def check_commits(system_arguments): return actions +def check_issues(system_arguments): + """Check the number of issues made in the github issue tracker""" + actions = [] + if system_arguments.issues is not None: + actions.append( + [ + INVOKE, + "invoke_issues_check", + [ + system_arguments.token, + system_arguments.repo, + system_arguments.name, + system_arguments.issues, + system_arguments.state, + ], + ] + ) + return actions + + +def check_issue_comments(system_arguments): + """Check the number of comments on issues made in the github issue tracker""" + actions = [] + if system_arguments.issue_comments is not None: + actions.append( + [ + INVOKE, + "invoke_issue_comments_check", + [ + system_arguments.token, + system_arguments.repo, + system_arguments.name, + system_arguments.issue_comments, + system_arguments.state, + ], + ] + ) + return actions + + def check_exists(system_arguments): """Check the existence of a file in directory and return desired actions""" actions = [] @@ -305,6 +345,8 @@ def check(system_arguments): "check_count_file", "check_count_command", "check_executes_command", + "check_issues", + "check_issue_comments", ] # iterate through all of the possible checks for a_check in checks: diff --git a/tests/test_arguments.py b/tests/test_arguments.py index d3a37d711..b1ac29807 100644 --- a/tests/test_arguments.py +++ b/tests/test_arguments.py @@ -544,6 +544,64 @@ def test_is_valid_comments_valid(chosen_arguments): assert verified_arguments is True +def test_is_valid_token_false(): + """Tests valid_token specifications when it args.token is None""" + # pylint: disable=too-few-public-methods + # pylint: disable=missing-docstring + class args: + token = None + + args = args() + expected_output = False + assert arguments.is_valid_token(args) == expected_output + + +def test_is_valid_repo_None(): + """Tests if repo does not have valid specifications""" + # pylint: disable=too-few-public-methods + # pylint: disable=missing-docstring + class args: + repo = None + + expected_output = False + assert arguments.is_valid_repo(args) == expected_output + + +def test_is_valid_name_false(): + """Tests is_valid_name when args.name is None""" + # pylint: disable=too-few-public-methods + # pylint: disable=missing-docstring + class args: + name = None + + expected_output = False + assert arguments.is_valid_name(args) == expected_output + + +def test_is_valid_state_True(): + """Tests if it is a valid state""" + + # pylint: disable=too-few-public-methods + # pylint: disable=missing-docstring + class args: + state = "something" + + expected_output = True + assert arguments.is_valid_state(args) == expected_output + + +def test_is_valid_state_False(): + """Tests if it is a valid state""" + + # pylint: disable=too-few-public-methods + # pylint: disable=missing-docstring + class args: + state = None + + expected_output = False + assert arguments.is_valid_state(args) == expected_output + + @pytest.mark.parametrize( "chosen_arguments", [ diff --git a/tests/test_invoke.py b/tests/test_invoke.py index 11e121da8..0b79f4415 100644 --- a/tests/test_invoke.py +++ b/tests/test_invoke.py @@ -32,6 +32,74 @@ def test_commit_checks_exact(reset_results_dictionary): assert details is not None +def test_invoke_issues_check_GitHub(): + """Checks to see if the github_token is invalid and expects False""" + github_token = "fake token" + repo_name = "fake_name" + username = "user" + expected_count = "0" + issue_state = "all" + expected_output = False + assert ( + invoke.invoke_issues_check( + github_token, repo_name, username, expected_count, issue_state + ) + == expected_output + ) + + +def test_invoke_issues_check_repo(): + """Checks to see if the github_token is invalid and expects False""" + TOKEN = "3e20125561f10fa4df42" + Next = "ac38d5bdd114df9a0ee8" + github_token = TOKEN + Next + repo_name = "fake_name" + username = "user" + expected_count = "0" + issue_state = "all" + expected_output = False + assert ( + invoke.invoke_issues_check( + github_token, repo_name, username, expected_count, issue_state + ) + == expected_output + ) + + +def test_invoke_issue_comments_check_GitHub(): + """Tests for an Invalid GitHub Token supplied""" + repo_name = "fake_name" + username = "user" + expected_count = "0" + issue_state = "all" + github_token = "fake_token" + expected_output = False + assert ( + invoke.invoke_issue_comments_check( + github_token, repo_name, username, expected_count, issue_state + ) + == expected_output + ) + + +def test_invoke_issue_comments_check_repo(): + """Tests for an Invalid GitHub Token supplied""" + TOKEN = "3e20125561f10fa4df42" + Next = "ac38d5bdd114df9a0ee8" + github_token = TOKEN + Next + repo_name = "fake_name" + username = "user" + expected_count = "0" + issue_state = "all" + expected_output = False + assert ( + invoke.invoke_issue_comments_check( + github_token, repo_name, username, expected_count, issue_state + ) + == expected_output + ) + + # pylint: disable=unused-argument # pylint: disable=redefined-outer-name def test_file_exists_in_directory_check(reset_results_dictionary, tmpdir): diff --git a/tests/test_issues.py b/tests/test_issues.py new file mode 100644 index 000000000..b7febc669 --- /dev/null +++ b/tests/test_issues.py @@ -0,0 +1,104 @@ +"""Test cases for the issues module""" + +from gator import issues + +# will need to not use this and take out the second half of the token later. +TOKEN = "3e20125561f10fa4df42" + + +def test_issue_made_pass(): + """Checks to ensure that issues are correctly being checked""" + out, num, err = issues.check_issues_made( + TOKEN + "ac38d5bdd114df9a0ee8", + "GatorEducator/test-repository", + "yeej2", + 1, + "all", + ) + assert out is True + assert num == 1 + assert err == 0 + + +def test_issues_made_fail(): + """Checks when issues_made is less than expected""" + out, num, err = issues.check_issues_made( + TOKEN + "ac38d5bdd114df9a0ee8", + "GatorEducator/test-repository", + "yeej2", + 2, + "all", + ) + assert out is False + assert num == 1 + assert err == 0 + + +def test_issue_comment_pass(): + """Checks to ensure that comments on issues are correctly being checked""" + out, num, err = issues.check_comments_made( + TOKEN + "ac38d5bdd114df9a0ee8", + "GatorEducator/test-repository", + "yeej2", + 1, + "all", + ) + assert out is True + assert num == 1 + assert err == 0 + + +def test_issue_comment_fail(): + """Checks to ensure that comments on issues are correctly being checked""" + out, num, err = issues.check_comments_made( + TOKEN + "ac38d5bdd114df9a0ee8", + "GatorEducator/test-repository", + "yeej2", + 2, + "all", + ) + assert out is False + assert num == 1 + assert err == 0 + + +def test_issue_invalid_token(): + """Checks to ensure that an incorrect token returns the correct error""" + __, __, err = issues.check_issues_made( + "aaa", "GatorEducator/test-repository", "yeej2", 1, "all" + ) + assert err == -1 + + +# pylint: disable=function-redefined +def test_issue_invalid_repo(): + """Checks to ensure that if there is an incorrect repo it returns the correct error""" + __, __, err = issues.check_issues_made( + TOKEN + "ac38d5bdd114df9a0ee8", "GatorEducator/gator", "yeej2", 1, "all" + ) + assert err == -2 + + +# def test_check_issues_made(): +# """Checks the final output of check_issues_made""" +# actual_output = issues.check_issues_made( +# TOKEN + "ac38d5bdd114df9a0ee8", "GatorEducator/test-repository", "yeej2", 1, "all" +# ) +# expected_output = + + +# pylint: disable=function-redefined +def test_comments_invalid_token(): + """Checks to ensure that if there is an incorrect token it returns the correct error""" + __, __, err = issues.check_comments_made( + "aaa", "GatorEducator/test-repository", "yeej2", 1, "all" + ) + assert err == -1 + + +def test_comments_invalid_repo(): + """Checks to ensure that if there is an incorrect repo it returns the correct error""" + __, __, err = issues.check_comments_made( + TOKEN + "ac38d5bdd114df9a0ee8", "GatorEducator/gator", "yeej2", 1, "all" + ) + assert err == -2 diff --git a/tests/test_orchestrate.py b/tests/test_orchestrate.py index f0ce1eb66..05155b298 100644 --- a/tests/test_orchestrate.py +++ b/tests/test_orchestrate.py @@ -102,6 +102,64 @@ def test_perform_actions_display_welcome_and_ready_check_commit( assert exit_code == 0 +TOKEN = "3e20125561f10fa4df42" + + +# pylint: disable=redefined-outer-name +# pylint: disable=bad-continuation +def test_perform_actions_display_welcome_and_ready_check_issues( + capsys, reset_results_dictionary +): + """Check argument verification, messages, and continue""" + chosen_arguments = [ + "--token", + TOKEN + "ac38d5bdd114df9a0ee8", + "--repo", + "GatorEducator/gatorgrader", + "--name", + "gkapfham", + "--issues", + "1", + "--state", + "all", + ] + exit_code = orchestrate.check(chosen_arguments) + captured = capsys.readouterr() + counted_newlines = captured.out.count("\n") + assert "GatorGrader" in captured.out + assert counted_newlines == 6 + assert exit_code == 0 + + +TOKEN = "3e20125561f10fa4df42" + + +# pylint: disable=redefined-outer-name +# pylint: disable=bad-continuation +def test_perform_actions_display_welcome_and_ready_check_issue_comments( + capsys, reset_results_dictionary +): + """Check argument verification, messages, and continue""" + chosen_arguments = [ + "--token", + TOKEN + "ac38d5bdd114df9a0ee8", + "--repo", + "GatorEducator/gatorgrader", + "--name", + "gkapfham", + "--issue-comments", + "1", + "--state", + "all", + ] + exit_code = orchestrate.check(chosen_arguments) + captured = capsys.readouterr() + counted_newlines = captured.out.count("\n") + assert "GatorGrader" in captured.out + assert counted_newlines == 6 + assert exit_code == 0 + + # pylint: disable=redefined-outer-name # pylint: disable=bad-continuation def test_perform_actions_display_welcome_and_ready_check_exists(