def _compile_project_source(self, project_name, dir_to_grade): """Compiles Java source code via javac. :param project_name: Name of the project being built :param dir_to_grade: Root of the directory tree where project files live :returns Number of compiler errors""" self._logger.debug(f'Compiling project source code: {dir_to_grade}') src_dir = PathManager.get_project_src_dir_name(project_name) path_dir = os.sep.join([str(dir_to_grade), src_dir]) full_classpath = PathManager.get_full_classpath(java_cp=f'.:{path_dir}', junit_cp=None) self._logger.debug(f'src_dir: {src_dir}') self._logger.debug(f'path_dir: {path_dir}') self._logger.debug(f'full_classpath: {full_classpath}') java_file_names = self._get_java_file_names(project_name, dir_to_grade) build_errors = 0 try: for src_file in java_file_names: result = subprocess.run(['javac', '-classpath', full_classpath, '-sourcepath', full_classpath, src_file], stdout=subprocess.PIPE, stderr=subprocess.PIPE) result_string = "OK" if result.returncode == 0 else "FAILED" file_name = Path(src_file).name self._logger.debug(f'...{file_name} => {result_string}') build_errors = build_errors + result.returncode except Exception as ex: self._logger.error("Exception caught while compiling source: {}".format(str(ex))) build_errors += 1 return build_errors
def run_project_unit_tests(self, email, project_name, dir_to_grade, test_class_name): """Runs the project's unit test. Assumes JUnit as testing framework. :param email: Project owner's email :param project_name: Project being graded :param dir_to_grade: Root of directory tree where project files live :param test_class_name: Name of the JUnit test suite, e.g., TestSuite, sans the .class extension :returns Ratio of passed test/all tests as a floating point number. 1.0 means all tests passed.""" self._logger.info( f'Running unit tests: {email}{os.sep}{project_name}{os.sep}{test_class_name}' ) # Determine proper paths and classes src_dir = PathManager.get_project_src_dir_name(project_name) path_dir = os.sep.join([str(dir_to_grade), src_dir]) full_classpath = PathManager.get_full_classpath( java_cp=f'.{os.pathsep}{path_dir}', junit_cp=None) test_suite_class = PathManager.get_student_test_suite(project_name) # Run the tests using JUnit's command-line runner results = subprocess.run([ 'java', '-cp', full_classpath, 'org.junit.runner.JUnitCore', test_suite_class ], stdout=subprocess.PIPE, stderr=subprocess.PIPE) # Process the result of running the tests. return \ self._process_test_results(test_suite_class, results)
def run_instructor_unit_tests(self, email, project_name, dir_to_grade, suite_dir, suite_class): """Runs the project's unit test. Assumes JUnit as testing framework. :param email: Project owner's email :param project_name: Project being graded :param dir_to_grade: Root of directory tree where project files live :param suite_dir: Full path to the JUnit test suite, e.g., Grading, sans the .class extention; :param suite_class: Name of test suite class, sans the .class extension :returns Ratio of passed test/all tests as a floating point number. 1.0 means all tests passed.""" # Determine proper paths for java runtime so that we can find test classes src_dir = PathManager.get_project_src_dir_name(project_name) path_dir = os.sep.join([str(dir_to_grade), src_dir]) full_classpath = PathManager.get_full_classpath( java_cp=f'.{os.pathsep}{path_dir}{os.pathsep}{suite_dir}', junit_cp=None) # Run the tests using JUnit's command-line runner results = subprocess.run([ 'java', '-cp', full_classpath, 'org.junit.runner.JUnitCore', suite_class ], stdout=subprocess.PIPE, stderr=subprocess.PIPE) # Process the result of running the tests. return \ self._process_test_results(suite_class, results)
def _get_project_file_names(self, project_name, dir_to_grade, fn_package, pattern='*.java'): """Helper function that fetches the names of the given project's files that match the specified pattern. :param project_name: Name of the project being built :param dir_to_grade: Root of the directory tree where project files live :param fn_package: Function called to retrieve files. One retrieves source names, the other test names :param pattern: The shell file pattern to use when determining which files to fetch. :returns list of file names""" src_dir_name = PathManager.get_project_src_dir_name(project_name) files_package = fn_package(project_name) files_path = PathManager.package_name_to_path_name(files_package) full_path = os.sep.join([str(dir_to_grade), src_dir_name, files_path]) file_names = glob.glob(os.sep.join([full_path, pattern])) return file_names
def _clone_project(self, project_name, emails, force): """Clones the given project for each email in the specified email file. :arg project_name: Name of the project to clone, e.g., pa1-review-student-master :arg emails: List of emails for which to clone projects :arg force: If true, causes clone to overwrite existing target directories""" if emails is None: self._logger.error( "Cannot clone projects without valid emails. Exiting.") sys.exit(-1) # Filter out blank lines owner_emails = [email for email in emails if len(email.strip(' ')) > 0] # Clone 'em self._logger.info('Cloning project: {}'.format(project_name)) for email in owner_emails: gitlab_project = self._server.get_user_project(email, project_name) if gitlab_project: dest_path_name = PathManager.build_dest_path_name( self._working_dir_name, email, project_name) self._server.clone_project(gitlab_project, dest_path_name, force) else: self._logger.warning( f"Project not found. Confirm server connectivity and login, project name '{project_name}' and email '{email}'." )
def clone_project(self, gitlab_project, dest_path_name, force=False): """git clone the given project from the GitLab server to the local computer. :returns None :param gitlab_project: GitLab project to clone. :param dest_path_name: Destination directory on the local computer to which the cloned files will be copied. :param force: True to force overwriting the destination directory if it already exists.""" try: PathManager.init_dest_path(dest_path_name, force) http_url = gitlab_project.http_url_to_repo self._logger.info('Cloning repo: {}...{}'.format( http_url, "(FORCED)" if force else '')) result = subprocess.run(['git', 'clone', http_url, dest_path_name], stdout=subprocess.PIPE, stderr=subprocess.PIPE) if result.returncode == 0: self._logger.info('Cloned OK') else: sresult = result.stderr.decode('utf-8') self._logger.warning(f'Clone war: {sresult}') except FileExistsError as fex: self._logger.warning(str(fex))
def _build_unit_test_classpath(self, project_name, dir_to_grade): """Builds the path to the project's unit tests based on the project name and components specified in the configuration file. :param project_name: Name of the project under test :param dir_to_grade: Full path to the directory that contains the project under test :return The full class path where Java can find the source under test and the tests themselves.""" # src directory under project/student email src_root_dir = os.sep.join([str(dir_to_grade), PathManager.get_project_src_dir_name(project_name)]) # full class name, including package, e.g., src/edu/wit... src_subdir_name = PathManager.package_name_to_path_name( PathManager.get_project_src_package(project_name)) src_package_dir = os.sep.join([src_root_dir, src_subdir_name]) # src directory and src directory + package name sources_dir = os.pathsep.join([src_root_dir, src_package_dir]) # Java and JUnit libs java_classpath = PathManager.get_full_classpath(java_cp=f'.:{sources_dir}', junit_cp=None) # Put them all together to build JUnit-based tests return java_classpath
def grade(self, email, project_name, dir_to_grade, project_due_dt, latest_commit_dt): """Grades a project for the specified owner (email). :param email: Project owner's email :param project_name: Name of the project being graded :param dir_to_grade: Root of directory tree containing project files :param project_due_dt: Project due datetime in UTC :param latest_commit_dt: Project's most recent commit datetime from server in UTC""" # Determines if the project is on time based on due datetime vs. latest commit datetime is_ontime, days, hours, mins = self._get_dt_diff_human_readable(project_due_dt, latest_commit_dt) # Information that goes into a grade record... grade_info = {'project_name': project_name, 'email': email, 'due_dt': project_due_dt, 'latest_commit_dt': latest_commit_dt, 'is_ontime': is_ontime, 'days': days, 'hours': hours, 'mins': mins} # Running list of notes notes = '' # Build source self._logger.debug(f'Building source: {dir_to_grade}') build_source_errors = self._builder.build_source(email, project_name, dir_to_grade) grade_info.update({'source_builds': build_source_errors == 0}) # Build student unit tests if build_source_errors == 0: self._logger.debug(f'Building student unit tests: {dir_to_grade}') build_tests_errors = self._builder.build_tests(email, project_name, dir_to_grade) grade_info.update({'student_tests_build': build_tests_errors == 0}) else: grade_info.update({'student_tests_build': 'NA'}) # Run project unit tests and calculate internal test ratio = passed tests / total tests if build_source_errors == 0 and build_tests_errors == 0: test_class_name = PathManager.get_student_test_class(project_name) if test_class_name and len(test_class_name) > 0: num_tests_run, test_ratio = \ self._run_project_unit_tests(email, project_name, dir_to_grade, test_class_name) if num_tests_run > 0: grade_info.update({'student_tests_ratio': test_ratio}) else: grade_info.update({'student_tests_ratio': 'No tests run. Check configuration file for proper test suite name.'}) else: self._logger.warning('Missing unit test class. No tests specified.') grade_info.update({'student_tests_ratio': 'No tests specified. Check configuration file.'}) else: # Cannot run unit tests due to build errors in source or in tests grade_info.update({'student_tests_ratio': 'No tests run due to build failures'}) # Run instructor (external) unit tests if there is an instructor test class defined in # the Proctor configuration file # # Note: The instructor unit test are assumed to have been compiled and ready to run # against the project. We do not build the instructor's tests. This could be added # as a feature in the future, should it prove necessary or valuable. if build_source_errors == 0: suite_dir, suite_class = PathManager.get_instructor_test_suite(project_name) if PathManager.instructor_test_suite_exists(suite_dir, suite_class): self._logger.info(f'Running instructor unit tests: {suite_dir}:{suite_class}') num_tests_run, test_ratio = \ self._run_instructor_unit_tests(email, project_name, dir_to_grade, suite_dir, suite_class) if num_tests_run > 0: grade_info.update({'instructor_tests_ratio': test_ratio}) else: grade_info.update({'instructor_tests_ratio': 'No tests run!'}) else: self._logger.warning('No instructor unit tests specified in the configuration file. Continuing.') grade_info.update({'instructor_tests_ratio': 'No tests specified. Check configuration file.'}) else: self._logger.info('Skipping instructor unit tests due to source build failures') grade_info.update({'instructor_tests_ratio': 'No tests run due to source build failures'}) # Record the results of grading this user's project in the gradebook. grade_info.update({'grade': 'TBD'}) grade_info.update({'notes': notes}) self._gradebook.record_grade(grade_info)