diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..f521262 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,24 @@ +name: Python Test + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + python-script-test: + runs-on: ubuntu-latest + steps: + - name: Checkout project sources + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: 3.8 + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + - name: Run tests + run: python TestMain.py diff --git a/.gitignore b/.gitignore index 4d2eed5..0afcf54 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,9 @@ ISSUES # macOS specific file -.DS_Store \ No newline at end of file +.DS_Store + +__pycache__/ + +#Ignore compiled class file +*.class \ No newline at end of file diff --git a/Keyvalue.py b/Keyvalue.py new file mode 100644 index 0000000..86f0d19 --- /dev/null +++ b/Keyvalue.py @@ -0,0 +1,18 @@ +from enum import Enum + +class JsonKeys(Enum): + ISSUE_ID = 'issue_id' + URL = 'url' + BRANCH = 'branch' + COMMIT_HASH = 'commit_hash' + PROJECT_NAME = 'project_name' + BUILD_COMMAND = 'build_command' + ROOT_DIR = 'root_dir' + PACKAGE = 'package' + TARGETS = 'targets' + METHOD_NAME = 'method' + FILE_NAME = 'file' + CF_Version = 'cf_version' + JAVA_VERSION = 'java_version' + NOTE = 'note' + SKIP = 'skip' diff --git a/TestMain.py b/TestMain.py new file mode 100644 index 0000000..f0c7023 --- /dev/null +++ b/TestMain.py @@ -0,0 +1,117 @@ +import unittest +import main +import shutil +import os +from Keyvalue import JsonKeys + +class TestMain(unittest.TestCase): + + @classmethod + def setUpClass(cls): + # cloning the specimin + main.clone_repository('https://github.com/kelloggm/specimin.git', 'resources') + cls.json_data = main.read_json_from_file('resources/test_data.json')[0] + cls.specimin_dir = "resources/specimin" + + @classmethod + def tearDownClass(cls): + # deleting specimin from resources + try: + shutil.rmtree('resources/specimin') + except Exception as e: + print(f"Error occurred {e}") + # removing any issue project cloned in resources + for root, dirs, files in os.walk('resources', topdown=False): + for dir_name in dirs: + if 'cf-' in dir_name: + dir_path = os.path.join(root, dir_name) + shutil.rmtree(dir_path) + print(f"Removed directory: {dir_path}") + + + def test_get_repository_name(self): + url = 'git@github.com:codespecs/daikon.git' + self.assertEqual(main.get_repository_name(url), 'daikon') + + url = 'git@github.com:kelloggm/specimin.git' + self.assertEqual(main.get_repository_name(url), 'specimin') + + url = 'git@github.com:typetools/checker-framework.git' + self.assertEqual(main.get_repository_name(url), 'checker-framework') + + url = 'git@github.com:awslabs/aws-kms-compliance-checker.git' + self.assertEqual(main.get_repository_name(url), 'aws-kms-compliance-checker') + + url = 'https://github.com/kelloggm/specimin.git' + self.assertEqual(main.get_repository_name(url), 'specimin') + + url = 'git@github.com:awslabs/aws-kms-compliance-checker.git' + self.assertNotEqual(main.get_repository_name(url), 'aws-km-compliance-checker') + + def test_build_specimin_command(self): + proj_name = 'cassandra' + root = 'src/java' + package = 'org.apache.cassandra.index.sasi.conf' + targets = [{ + "method": "getMode(ColumnMetadata, Map)", + "file": "IndexMode.java" + }] + specimin_dir = 'user/specimin' + target_dir = 'user/ISSUES/cf-6077' + command = main.build_specimin_command(proj_name, target_dir, specimin_dir, root, package, targets) + target_command = '' + with open('resources/specimin_command_cf-6077.txt','r') as file: + target_command = file.read() + self.assertEqual(command, target_command) + # not executing since this crashes specimin + proj_name = 'kafka-sensors' + root = 'src/main/java/' + package = 'com.fillmore_labs.kafka.sensors.serde.confluent.interop' + targets = [{ + "method": "transform(String, byte[])", + "file": "Avro2Confluent.java" + }] + specimin_dir = 'user/specimin' + target_dir = 'user/ISSUES/cf-6019' + command = main.build_specimin_command(proj_name, target_dir, specimin_dir, root, package, targets) + with open('resources/specimin_command_cf-6019.txt','r') as file: + target_command = file.read() + self.assertEqual(command, target_command) + #not executing since it crashes specimin. + + # make + issue_name = self.json_data[JsonKeys.ISSUE_ID.value] + main.create_issue_directory('resources', issue_name) + self.assertTrue(os.path.exists('resources/cf-1291/input')) + main.clone_repository(self.json_data[JsonKeys.URL.value], f"resources/{issue_name}/input") + + project_name = main.get_repository_name(self.json_data[JsonKeys.URL.value]) + + self.assertTrue(main.checkout_commit(self.json_data[JsonKeys.COMMIT_HASH.value],f"resources/{issue_name}/input/{project_name}")) + self.assertTrue(main.is_git_directory(f"resources/{issue_name}/input/{project_name}")) + + command = main.build_specimin_command(project_name, f"resources/{issue_name}", self.specimin_dir, self.json_data[JsonKeys.ROOT_DIR.value], self.json_data[JsonKeys.PACKAGE.value], self.json_data[JsonKeys.TARGETS.value]) + print(command) + result = main.run_specimin(command, self.specimin_dir) + self.assertTrue(result) + + def test_run_specimin(self): + proj_name = 'test_proj' + root = '' + package = 'com.example' + targets = [{ + "method": "bar()", + "file": "Simple.java" + }] + specimin_dir = 'resources/specimin' + target_dir = 'resources/onefilesimple' + + command = main.build_specimin_command(proj_name, target_dir, specimin_dir, root, package, targets) + result = main.run_specimin(command, 'resources/specimin') + self.assertTrue(result) + + + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/main.py b/main.py index 599716a..233f437 100644 --- a/main.py +++ b/main.py @@ -1,10 +1,25 @@ import json import os import subprocess +from Keyvalue import JsonKeys -issue_directory = 'ISSUES' + +issue_folder_dir = 'ISSUES' +specimin_input = 'input' +specimin_output = 'output' +specimin_project_name = 'specimin' +specimin_source_url = 'https://github.com/kelloggm/specimin.git' def read_json_from_file(file_path): + ''' + Parse a json file. + + Parameters: + file_path (path): Path to the json file + + Retruns: + { }: Parsed json data + ''' try: with open(file_path, 'r') as file: json_data = json.load(file) @@ -16,69 +31,264 @@ def read_json_from_file(file_path): print(f"File not found: {file_path}") return None -def create_directory(issue_container_dir, issue_id): + +def get_repository_name(github_ssh: str): + ''' + Extract the repository name from github ssh + Parameters: + github_ssh (str): A valid github ssh + + Returns: repository name + ''' + repository_name = os.path.splitext(os.path.basename(github_ssh))[0] + return repository_name + +def create_issue_directory(issue_container_dir, issue_id): + ''' + Creates a directory to store a SPECIMIN target project. Example: issue_id of cf-111 will create + a cf-111 directory inside 'issue_container_dir'. Two other directory (input and output inside) will + be created inside 'issue_container_dir/issue_id' directory. Target project is cloned inside + 'issue_container_dir/issue_id/input' directory. SPECIMIN output is stored inside 'issue_container_dir/issue_id/output' + directory + + issue_container_dir + |--- issue_id + | |--- input + | |--- output + + Parameters: + issue_container_dir (str): The directory where new directory is created + issue_id (str): Name of the directory to be created + + Returns: + specimin_input_dir (str): A target directory of SPECIMIN. (issue_container_dir/issue_id/input) + ''' issue_directory_name = os.path.join(issue_container_dir, issue_id) os.makedirs(issue_directory_name, exist_ok=True) - specimin_input_dir = os.path.join(issue_directory_name, "input") - specimin_output_dir = os.path.join(issue_directory_name, "output") + specimin_input_dir = os.path.join(issue_directory_name, specimin_input) + specimin_output_dir = os.path.join(issue_directory_name, specimin_output) os.makedirs(specimin_input_dir, exist_ok=True) os.makedirs(specimin_output_dir, exist_ok=True) return specimin_input_dir -def clone_repository(url, directory): #TODO: parallel cloning task - subprocess.run(["git", "clone", url, directory]) + +def is_git_directory(dir): + ''' + Check whether a directory is a git directory + Parameters: + dir: path of the directory + Returns: + booleans + ''' + git_dir_path = os.path.join(dir, '.git') + return os.path.exists(git_dir_path) and os.path.isdir(git_dir_path) + +def clone_repository(url, directory): + ''' + Clone a repository from 'url' in 'directory' + + Parameters: + url (str): repository url + directory (str): directory to clone in + ''' + project_name = get_repository_name(url) + if (os.path.exists(f"{directory}/{project_name}")): + print(f"{project_name} repository already exists. Aborting cloning") + subprocess.run(["git", "clone", url], cwd=directory) def change_branch(branch, directory): - pass + ''' + Checkout a branch of a git repository + + Parameters: + branch (str): branch name + directory (str): local directory of the git repository + ''' + if not is_git_directory(directory): + raise ValueError(f"{directory} is not a valid git directory") + command = ["git", "checkout", f"{branch}"] + subprocess.run(command, cwd=directory) def checkout_commit(commit_hash, directory): + ''' + Checkout a commit of a git repository + + Parameters: + commit_hash (str): commit hash + directory (str): local directory of the git repository + ''' + if not is_git_directory(directory): + raise ValueError(f"{directory} is not a valid git directory") + command = ["git", "checkout", commit_hash] result = subprocess.run(command, cwd=directory, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - # Check if the command was successful if result.returncode == 0: print(f"Successfully checked-out commit {commit_hash} in {directory}") else: print(f"Failed to checkout commit {commit_hash} in {directory}") - + return result.returncode == 0 if True else False + +def perform_git_pull (directory): + ''' + Pull latest of a git repository + + Parameters: + directory (str): local directory of the git repository + ''' + command=["git", "pull", "origin", "--rebase"] + subprocess.run(command, cwd=directory) + +def clone_specimin(path_to_clone, url): + ''' + Clone the latest Specimin project from github + + Parameters: + path_to_clone (str): Path where Specimin is to be clonned + url (str): url of specimin + ''' + spcimin_source_path = os.path.join(issue_folder_dir, specimin_project_name) + if (os.path.exists(spcimin_source_path)) and os.path.isdir(spcimin_source_path): + perform_git_pull(spcimin_source_path) + else: + clone_repository(url, path_to_clone) + + +def build_specimin_command(project_name: str, + issue_input_dir: str, + specimin_dir: str, + root_dir: str, + package_name: str, + targets: list): + ''' + Build the gradle command to execute Specimin on target project + + issue_container_dir(ISSUES) + |--- issue_id(cf-1291) + | |--- input ---> it contains the git repository of a target project + | | |----nomulus/core/src/main/java/ ---> this is the root directory of a package + | | |---package_path/file.java (daikon/chicory/PureMethodInfo.java) --> a target file + | |--- output --> Contains minimization result of Specimin + + + Parameters: + project_name (str): Name of the target project. Example: daikon + issue_input_dir (str): path of the target project directory. Ex: ISSUES/cf-1291 + specimin_dir (str): Specimin directory path + root_dir (str): A directory path relative to the project base directory where java package stored. + package_name (str): A valid Java package + targets ({'method': '', 'file': ''}) : target java file and method name data + + Retruns: + command (str): The gradle command of SPECIMIN for the issue. + ''' + + relative_path_of_target_dir = os.path.relpath(issue_input_dir, specimin_dir) + + output_dir = os.path.join(relative_path_of_target_dir, specimin_output) + root_dir = os.path.join(relative_path_of_target_dir, specimin_input, project_name, root_dir) + root_dir = root_dir.rstrip('/') + os.sep + + dot_replaced_package_name = package_name.replace('.', '/') + + target_file_list = [] + target_method_list = [] + + for target in targets: + method_name = target[JsonKeys.METHOD_NAME.value] + file_name = target[JsonKeys.FILE_NAME.value] + + if file_name: + qualified_file_name = os.path.join(dot_replaced_package_name, file_name) + target_file_list.append(qualified_file_name) -def run_specimin(build_command): - pass + if method_name: + qualified_method_name = package_name + "." + os.path.splitext(file_name)[0]+ "#" + method_name + target_method_list.append(qualified_method_name) -def performEvaluation(issue): - issue_id = issue['issue_id'] - url = issue['url'] - branch = issue['branch'] - commit_hash = issue['commitHash'] - specimin_command = issue['specimin_command'] + output_dir_subcommand = "--outputDirectory" + " " + f"\"{output_dir}\"" + root_dir_subcommand = "--root" + " " + f"\"{root_dir}\"" - input_dir = create_directory(issue_directory, issue_id) + target_file_subcommand = "" + for file in target_file_list: + target_file_subcommand += "--targetFile" + " " + f"\"{file}\"" + + target_method_subcommand = "" + for method in target_method_list: + target_method_subcommand += "--targetMethod" + " " + f"\"{method}\"" + + command_args = root_dir_subcommand + " " + output_dir_subcommand + " " + target_file_subcommand + " " + target_method_subcommand + command = "./gradlew" + " " + "run" + " " + "--args=" + f"\'{command_args}\'" + + return command + +def run_specimin(command, directory): + ''' + Execute SPECIMIN on a target project + + Parameters: + command (str): The gradle command to run specimin + directory (str): The base directory of the specimin repository + + Returns: + boolean: True/False based on successful execution of SPECIMIN + ''' + result = subprocess.run(command, cwd=directory, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) # TODO: + if result.returncode == 0: + return True + else: + return False + + +def performEvaluation(issue_data): + ''' + For each issue data, execute SPECIMIN on a target project. + + Parameters: + issue ({}): json data associated with an issue + ''' + + issue_id = issue_data[JsonKeys.ISSUE_ID.value] + url = issue_data[JsonKeys.URL.value] + branch = issue_data[JsonKeys.BRANCH.value] + commit_hash = issue_data[JsonKeys.COMMIT_HASH.value] + + input_dir = create_issue_directory(issue_folder_dir, issue_id) # ../cf-12/input clone_repository(url, input_dir) # TODO: check if clonning is successful. + repo_name = get_repository_name(url) if branch: - change_branch(input_dir, branch) + change_branch(branch, f"{input_dir}/{repo_name}") if commit_hash: - checkout_commit(commit_hash, input_dir) + checkout_commit(commit_hash, f"{input_dir}/{repo_name}") + + specimin_command = build_specimin_command(repo_name, os.path.join(issue_folder_dir, issue_id), os.path.join(issue_folder_dir, specimin_project_name), issue_data[JsonKeys.ROOT_DIR.value], issue_data[JsonKeys.PACKAGE.value], issue_data[JsonKeys.TARGETS.value]) - success = run_specimin(specimin_command) + success = run_specimin(specimin_command, os.path.join(issue_folder_dir, specimin_project_name)) if success: print(f"Test {issue_id} successfully completed.") else: print(f"Test {issue_id} failed.") + def main(): - - json_file_path = 'resources/test_data.json' + ''' + Main method of the script. It iterates over the json data and perform minimization for each cases. + ''' + os.makedirs(issue_folder_dir, exist_ok=True) # create the issue holder directory + clone_specimin(issue_folder_dir, specimin_source_url) + json_file_path = 'resources/test_data.json' parsed_data = read_json_from_file(json_file_path) if parsed_data: for issue in parsed_data: performEvaluation(issue) + if __name__ == "__main__": main() \ No newline at end of file diff --git a/resources/onefilesimple/input/test_proj/com/example/Simple.java b/resources/onefilesimple/input/test_proj/com/example/Simple.java new file mode 100644 index 0000000..a6d461d --- /dev/null +++ b/resources/onefilesimple/input/test_proj/com/example/Simple.java @@ -0,0 +1,13 @@ +package com.example; + +class Simple { + // Target method. + void bar() { + Object obj = new Object(); + obj = baz(obj); + } + + Object baz(Object obj) { + return obj.toString(); + } +} diff --git a/resources/specimin_command_cf-6019.txt b/resources/specimin_command_cf-6019.txt new file mode 100644 index 0000000..125f64e --- /dev/null +++ b/resources/specimin_command_cf-6019.txt @@ -0,0 +1 @@ +./gradlew run --args='--root "../ISSUES/cf-6019/input/kafka-sensors/src/main/java/" --outputDirectory "../ISSUES/cf-6019/output" --targetFile "com/fillmore_labs/kafka/sensors/serde/confluent/interop/Avro2Confluent.java" --targetMethod "com.fillmore_labs.kafka.sensors.serde.confluent.interop.Avro2Confluent#transform(String, byte[])"' \ No newline at end of file diff --git a/resources/specimin_command_cf-6077.txt b/resources/specimin_command_cf-6077.txt new file mode 100644 index 0000000..5ee2143 --- /dev/null +++ b/resources/specimin_command_cf-6077.txt @@ -0,0 +1 @@ +./gradlew run --args='--root "../ISSUES/cf-6077/input/cassandra/src/java/" --outputDirectory "../ISSUES/cf-6077/output" --targetFile "org/apache/cassandra/index/sasi/conf/IndexMode.java" --targetMethod "org.apache.cassandra.index.sasi.conf.IndexMode#getMode(ColumnMetadata, Map)"' \ No newline at end of file diff --git a/resources/test_data.json b/resources/test_data.json index ba2e609..472f6fd 100644 --- a/resources/test_data.json +++ b/resources/test_data.json @@ -1,14 +1,21 @@ [ { "issue_id" : "cf-1291", - "url": "git@github.com:codespecs/daikon.git", + "url": "https://github.com/codespecs/daikon.git", "branch": "", - "commitHash":"15d7c2d84", + "commit_hash": "15d7c2d84", "project_name": "daikon", "build_command": "this can be cf command or java", - "specimin_command": "command to run specimin", - "note": "", + "root_dir": "java", + "package": "daikon.chicory", + "targets": [ + { + "method": "executePureMethod(Method, Object, Object[])", + "file": "PureMethodInfo.java" + } + ], "cf_version": "2.1.10", - "java_version":"" + "java_version": "", + "note": "" } ] \ No newline at end of file