Esempio n. 1
0
    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
Esempio n. 2
0
    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)
Esempio n. 3
0
    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)
Esempio n. 4
0
 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
Esempio n. 5
0
    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}'."
                )
Esempio n. 6
0
 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))
Esempio n. 7
0
    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
Esempio n. 8
0
    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)