diff --git a/README.md b/README.md index 527aa88..63eadc3 100644 --- a/README.md +++ b/README.md @@ -3,3 +3,60 @@ Scripts for preparing student programs for grading. Scripts convert student repository code to a PDF file for grading. +## Example + +In this example we have the following structure: + + hw1 + ├── config.json + └── submissions + ├── jh + │   └── Lab10Code + │   ├── Employee.java + │   ├── Faculty.java + │   ├── Person.java + │   ├── Staff.java + │   └── Student.java + └── wk + └── Lab10Code + ├── Employee.java + ├── Faculty.java + ├── Person.java + ├── Staff.java + └── Student.java + + +`config.json` contains the following: + + { + "directory" : "/Users/karl/GoogleDrive/Courses/CS-140/StudentSubmissions/S1-2014/grading/Lab10", + "files" : [ + "Lab10Code/Person.java", + "Lab10Code/Student.java", + "Lab10Code/Employee.java", + "Lab10Code/Faculty.java", + "Lab10Code/Staff.java" + ] + } + +* `directory` - Path to the directory containing student submission + subdirectories. Paths are relative to the configuration file's location. +* `files` - List of files to process for each submission directory. Paths are + relative to each student's submission directory. + +`submissions` contians a subdirectory for each students' work. The names of each +subdirectories is not important. In this example they are the initials of each +student. + +We process the assignment as follows: + + $ python path/to/assignmentconvert.py hw1/config.json + +## Run tests + + $ cd grading-scripts + $ python run_tests.py + +## Writting a processor + +See `assignmentconvert.py` for an example. diff --git a/assignment.py b/assignment.py deleted file mode 100644 index a0da186..0000000 --- a/assignment.py +++ /dev/null @@ -1,122 +0,0 @@ -# Copyright (C) 2014 Karl R. Wurst -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA -import pathlib -import json -import os - - -class Assignment(object): - '''Provides traversal and path resolution for visitors over an assignment - directory. - ''' - - def __init__(self, config_file, verbose=False, cd=True): - self._config_file = pathlib.Path(config_file).resolve() - self._verbose = verbose - self._cd = cd - self._config_dict = None - self._submission_directories = [] - - self._load_config() - self._load_submission_directories() - - def _load_config(self): - self._config_dict = self._read_config() - self._config_dict['directory'] = self._resolve_path( - self._config_dict['directory'], - root=self._config_file.parent - ) - self._config_dict['files'] = [ - pathlib.Path(file_) for file_ in self._config_dict['files'] - ] - - def _read_config(self): - config = None - with self._config_file.open() as file_: - config = json.load(file_) - return config - - def _load_submission_directories(self): - self._submission_directories = [ - self._resolve_path(d, root=self._config_dict['directory']) - for d in self._config_dict['directory'].glob('*') - if d.is_dir() - ] - - @staticmethod - def _resolve_path(path, root=None): - path = pathlib.Path(path) - if path.is_absolute(): - return path - elif root is None: - return path.resolve() - else: - return (pathlib.Path(root).resolve() / path).resolve() - - def accept(self, visit, cd=None): - ''' - Calls visit(directory, files) for each submission directory. Directory - is a pathlib.Path that is the submission directory being visited. Files - is a list of pathlib.Path objects that are the files within the - directory to process. - - visit: callable - - cd: boolean - defaults True - uses accept to change into each - submission directory. - ''' - self._cd = cd if cd is not None else self._cd - for directory in self._submission_directories: - self._notify_start_process_directory(directory) - self._process_directory(visit, directory) - self._notify_end_process_directory(directory) - - def _notify_start_process_directory(self, directory): - if self._verbose: - print('Processing', directory) - - def _notify_end_process_directory(self, directory): - if self._verbose: - print('Done processing', directory) - - def _process_directory(self, visit, directory): - self._enter_directory(directory) - visit(directory, self._get_resolved_files(directory)) - self._exit_directory() - - def _enter_directory(self, directory): - if self._cd: - self._original_directory = pathlib.Path.cwd() - os.chdir(str(directory)) - - def _exit_directory(self): - if self._cd: - os.chdir(str(self._original_directory)) - - def _get_resolved_files(self, root): - resolved_or_none = [ - self._resolve_path_or_none(f, root=root) - for f in self._config_dict['files']] - resolved = [f for f in resolved_or_none if f is not None] - return resolved - - def _resolve_path_or_none(self, path, root=None): - resolved = None - try: - resolved = self._resolve_path(path, root=root) - except FileNotFoundError: - print('Not found: ' + str(root/path)) - return resolved diff --git a/assignmentconvert.py b/collect_pdfs.py similarity index 57% rename from assignmentconvert.py rename to collect_pdfs.py index 388f233..aabe43f 100644 --- a/assignmentconvert.py +++ b/collect_pdfs.py @@ -13,30 +13,17 @@ # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA -import argparse -import os -from assignment import Assignment -from command import Command +from submissions import SubmissionProcessor -class LabConvert(object): - +class LabConvert(SubmissionProcessor): def __init__(self): - parser = argparse.ArgumentParser() - parser.add_argument('config', help='JSON configuration file') - parser.add_argument( - '-v', '--verbose', - help='increase output verbosity', - action='store_true' - ) - args = parser.parse_args() - Command.set_default_verbosity(args.verbose) - self._a2pdf = Command( - 'a2pdf --noperl-syntax --noline-numbers "{ins}" -o "{ins}.pdf"') - self._pdfcat = Command('pdftk "{ins}" cat output "{outs}"') - self._create_log = Command('git log > log.txt') - self._rm = Command('rm "{ins}"') - Assignment(args.config).accept(self.process_submission) + super().__init__(self) + self._a2pdf = self.command( + 'a2pdf --noperl-syntax --noline-numbers {ins} -o {ins}.pdf') + self._pdfcat = self.command('pdftk {ins} cat output {outs}') + self._create_log = self.command('git log > log.txt') + self._rm = self.command('rm {ins}') def process_submission(self, directory, files): self._create_log() @@ -49,4 +36,4 @@ def process_submission(self, directory, files): if __name__ == '__main__': - LabConvert() + LabConvert().run() diff --git a/command.py b/command.py deleted file mode 100644 index 7f0f674..0000000 --- a/command.py +++ /dev/null @@ -1,79 +0,0 @@ -# Copyright (C) 2014 Karl R. Wurst -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA -import argparse -import os -import assignment - - -class Command(object): - ''' - Command represents a reusable shell command. - - Examples creating a command: - - concat = Command('cat "{ins}" > "{outs}"') - concat2 = Command('cat "{ins}" > "{ins}.2"') - - IMPORTANT: Always encluse placeholders in double quotes. - - Example calls: - - concat('f1', 'f2') # copies f1 to f2 - concat(['f1', 'f2'], 'f3') # lists, concats f1 and f2 into f3 - concat(pathlib.Path('f1'), 'f2') # you may pass Paths. - concat(o1, o2) # o1 and o2 will be converted to a string via str(). - concat(Path('.').glob('*.txt'), 'out.txt') # globbing - concat(outs='f3', ins=['f1', 'f2']) # named parameters - concat2('f1') # copies f1 to f1.2 ; optional parameters - concat2.each(['f1', 'f2]) # copies f1 to f1.2 and f2 to f2.2 - concat.each(['f1', 'f2'], ['f3', 'f4']) # copies f1 to f3 and f2 to f4 - ''' - _default_verbosity = False - - @classmethod - def set_default_verbosity(cls, flag): - cls._default_verbosity = flag - - def __init__(self, command_string, verbose=None): - self._verbose = \ - verbose if verbose is not None else Command._default_verbosity - self._command_string = command_string - - def __call__(self, ins=None, outs=None): - ins = self._format_args(ins) - outs = self._format_args(outs) - command = self._command_string.format(ins=ins, outs=outs) - self._run_command(command) - - def _format_args(self, args): - if args is None: - return None - if isinstance(args, list): - return '" "'.join([str(i) for i in args]) - elif not isinstance(args, str): - return str(args) - return args - - def _run_command(self, command): - if self._verbose: - print(command) - os.system(command) - - def each(self, ins=None, outs=None): - for i in range(len(ins)): - ins_i = ins[i] - outs_i = outs[i] if isinstance(outs, list) else outs - self(ins_i, outs_i) diff --git a/lab10config.json b/configs/lab10config.json similarity index 100% rename from lab10config.json rename to configs/lab10config.json diff --git a/lab11config.json b/configs/lab11config.json similarity index 100% rename from lab11config.json rename to configs/lab11config.json diff --git a/lab1config.json b/configs/lab1config.json similarity index 100% rename from lab1config.json rename to configs/lab1config.json diff --git a/lab2config.json b/configs/lab2config.json similarity index 100% rename from lab2config.json rename to configs/lab2config.json diff --git a/lab3config.json b/configs/lab3config.json similarity index 100% rename from lab3config.json rename to configs/lab3config.json diff --git a/lab4config.json b/configs/lab4config.json similarity index 100% rename from lab4config.json rename to configs/lab4config.json diff --git a/lab5config.json b/configs/lab5config.json similarity index 100% rename from lab5config.json rename to configs/lab5config.json diff --git a/lab6config.json b/configs/lab6config.json similarity index 100% rename from lab6config.json rename to configs/lab6config.json diff --git a/lab7config.json b/configs/lab7config.json similarity index 100% rename from lab7config.json rename to configs/lab7config.json diff --git a/lab9config.json b/configs/lab9config.json similarity index 100% rename from lab9config.json rename to configs/lab9config.json diff --git a/project1config.json b/configs/project1config.json similarity index 100% rename from project1config.json rename to configs/project1config.json diff --git a/project2config.json b/configs/project2config.json similarity index 100% rename from project2config.json rename to configs/project2config.json diff --git a/project3config.json b/configs/project3config.json similarity index 100% rename from project3config.json rename to configs/project3config.json diff --git a/project4config.json b/configs/project4config.json similarity index 100% rename from project4config.json rename to configs/project4config.json diff --git a/run_tests.py b/run_tests.py new file mode 100644 index 0000000..1130550 --- /dev/null +++ b/run_tests.py @@ -0,0 +1,23 @@ +# Copyright (C) 2014 Karl R. Wurst +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + +from unittest import TestLoader +from unittest.runner import TextTestRunner +from os.path import dirname, abspath + + +if __name__ == '__main__': + TextTestRunner().run(TestLoader().discover(abspath(dirname(__file__)))) diff --git a/submissions.py b/submissions.py new file mode 100644 index 0000000..0ebaba9 --- /dev/null +++ b/submissions.py @@ -0,0 +1,239 @@ +# Copyright (C) 2014 Karl R. Wurst +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA +import argparse +import logging +import json +import os +import pathlib + + +class SubmissionProcessor(object): + ''' + Base class for submission processors. To use: + 1) Inherit. + 2) Call super constructor. + 3) Optionally set self.cd = False to prevent changing into each + submission directory before calling process_submissions. + 4) Implement process_submission. + ''' + def __init__(self): + self.cd = True + self._arguments = CommandLineArguments() + self._init_logging() + self._assignment = Assignment(self._arguments.config) + + def _init_logging(self): + if self._arguments.verbose: + logging.basicConfig(level='DEBUG') + elif self._arguments.brief: + logging.basicConfig(level='WARNING') + else: + logging.basicConfig(level='INFO') + + def run(self): + self._assignment.accept(self.process_submission, self.cd) + + def process_submission(self, directory, files): + ''' + Called once for each submission directory. `directory` is a + `pathlib.Path` to the submission directory. `files` is a possibly empy + list of `pathlib.Path`s to files in `directory` to process. The paths + in `files` have been resolved against directory. All `files` exist. + ''' + raise NotImplementedError('Must implement.') + + def command(self, shell_string): + ''' + Returns a `Command` for the given `shell_string`. + ''' + return Command(shell_string) + + +class CommandLineArguments(object): + def __init__(self): + self._parser = argparse.ArgumentParser() + self._parser.add_argument('config', help='JSON configuration file') + self._parser.add_argument( + '-v', '--verbose', + help='increase output verbosity', + action='store_true', + default=False + ) + self._parser.add_argument( + '-b', '--brief', + help='decrease output verbosity', + action='store_true', + default=False + ) + self._parser.parse_args(namespace=self) + + +class Assignment(object): + + def __init__(self, config_file, cd=True): + self._config_file = pathlib.Path(config_file).resolve() + self._cd = cd + self._config_dict = None + self._submission_directories = [] + + self._load_config() + self._load_submission_directories() + + def _load_config(self): + self._config_dict = self._read_config() + self._config_dict['directory'] = self._resolve_path( + self._config_dict['directory'], + root=self._config_file.parent + ) + self._config_dict['files'] = [ + pathlib.Path(file_) for file_ in self._config_dict['files'] + ] + + def _read_config(self): + config = None + with self._config_file.open() as file_: + config = json.load(file_) + return config + + def _load_submission_directories(self): + self._submission_directories = [ + self._resolve_path(d, root=self._config_dict['directory']) + for d in self._config_dict['directory'].glob('*') + if d.is_dir() + ] + + @staticmethod + def _resolve_path(path, root=None): + path = pathlib.Path(path) + if path.is_absolute(): + return path + elif root is None: + return path.resolve() + else: + return (pathlib.Path(root).resolve() / path).resolve() + + def accept(self, visit, cd=None): + ''' + Calls visit(directory, files) for each submission directory. Directory + is a pathlib.Path that is the submission directory being visited. Files + is a list of pathlib.Path objects that are the files within the + directory to process. + + visit: callable + + cd: boolean - defaults True - uses accept to change into each + submission directory. + ''' + self._cd = cd if cd is not None else self._cd + for directory in self._submission_directories: + self._notify_start_process_directory(directory) + self._process_directory(visit, directory) + self._notify_end_process_directory(directory) + + def _notify_start_process_directory(self, directory): + logging.info('Processing {directory}'.format(directory=directory)) + + def _notify_end_process_directory(self, directory): + logging.info('Done processing {directory}'.format(directory=directory)) + + def _process_directory(self, visit, directory): + self._enter_directory(directory) + visit(directory, self._get_resolved_files(directory)) + self._exit_directory() + + def _enter_directory(self, directory): + if self._cd: + self._original_directory = pathlib.Path.cwd() + os.chdir(str(directory)) + + def _exit_directory(self): + if self._cd: + os.chdir(str(self._original_directory)) + + def _get_resolved_files(self, root): + resolved_or_none = [ + self._resolve_path_or_none(f, root=root) + for f in self._config_dict['files']] + resolved = [f for f in resolved_or_none if f is not None] + return resolved + + def _resolve_path_or_none(self, path, root=None): + resolved = None + try: + resolved = self._resolve_path(path, root=root) + except FileNotFoundError: + logging.warning('Not found: {path}'.format(path=str(root/path))) + return resolved + + +class Command(object): + ''' + Command represents a reusable shell command. + + Examples creating a command: + + concat = Command('cat {ins} > {outs}') + concat2 = Command('cat {ins} > {ins}.2') + + {ins} and {outs} will be replaced by the parameters passed to the command + when called. These parameters will be quoted when substituted in. So, do not + quote. + + Example calls: + + concat('f1', 'f2') # copies f1 to f2 + concat(['f1', 'f2'], 'f3') # lists, concats f1 and f2 into f3 + concat(pathlib.Path('f1'), 'f2') # you may pass Paths. + concat(o1, o2) # o1 and o2 will be converted to a string via str(). + concat(Path('.').glob('*.txt'), 'out.txt') # globbing + concat(outs='f3', ins=['f1', 'f2']) # named parameters + concat2('f1') # copies f1 to f1.2 ; optional parameters + concat2.each(['f1', 'f2]) # copies f1 to f1.2 and f2 to f2.2 + concat.each(['f1', 'f2'], ['f3', 'f4']) # copies f1 to f3 and f2 to f4 + ''' + + def __init__(self, command_string): + self._command_string = command_string + + def __call__(self, ins=None, outs=None): + ins = self._format_args(ins) + outs = self._format_args(outs) + command = self._command_string.format(ins=ins, outs=outs) + self._run_command(command) + + def _format_args(self, args): + if args is None: + return None + if isinstance(args, list): + return ' '.join([self._format_args(arg) for arg in args]) + elif not isinstance(args, str): + args_string = str(args) + else: + args_string = args + args_string = args_string.replace('"', '\\"') + args_string = '"' + args_string + '"' + return args_string + + + def _run_command(self, command): + logging.debug(command) + os.system(command) + + def each(self, ins=None, outs=None): + for i in range(len(ins)): + ins_i = ins[i] + outs_i = outs[i] if isinstance(outs, list) else outs + self(ins_i, outs_i) diff --git a/test_assignment.py b/test_assignment.py deleted file mode 100644 index 95f68ac..0000000 --- a/test_assignment.py +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright (C) 2014 Karl R. Wurst -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA -import unittest -import sandbox -import os.path -from assignment import Assignment - - -class test_Assignment(unittest.TestCase): - - def setUp(self): - self.collector = Collector() - - def test_each(self): - the_assignment = Assignment(sandbox.dir('assignment1.json')) - the_assignment.accept(self.collector.visit) - self.assertPathsExist() - - def assertPathsExist(self): - self.assertTrue( - self.collector.directories, msg="No directories collected.") - self.assertTrue(self.collector.files, msg="No files collected.") - for d in self.collector.directories: - self.assertTrue( - d.exists(), msg="Directory does not exist: " + str(d)) - self.assertTrue(d.is_dir(), msg="Not a directory: " + str(d)) - for f in self.collector.files: - self.assertTrue(f.exists(), msg="File does not exist: " + str(f)) - self.assertTrue(f.is_file(), msg="Not a file: " + str(f)) - - -class Collector(object): - def __init__(self): - self.directories = [] - self.files = [] - - def visit(self, submission_directory, files_to_collect): - self.directories.append(submission_directory) - for file in files_to_collect: - self.files.append(file) - - -if __name__ == '__main__': - unittest.main() diff --git a/sandbox/assignment1/student1/file1.txt b/tests/__init__.py similarity index 100% rename from sandbox/assignment1/student1/file1.txt rename to tests/__init__.py diff --git a/sandbox.py b/tests/sandbox.py similarity index 100% rename from sandbox.py rename to tests/sandbox.py diff --git a/sandbox/assignment1.json b/tests/sandbox/assignment1.json similarity index 100% rename from sandbox/assignment1.json rename to tests/sandbox/assignment1.json diff --git a/sandbox/assignment1/student2/file1.txt b/tests/sandbox/assignment1/student1/file1.txt similarity index 100% rename from sandbox/assignment1/student2/file1.txt rename to tests/sandbox/assignment1/student1/file1.txt diff --git a/sandbox/assignment1/student1/subdir/file2.txt b/tests/sandbox/assignment1/student1/subdir/file2.txt similarity index 100% rename from sandbox/assignment1/student1/subdir/file2.txt rename to tests/sandbox/assignment1/student1/subdir/file2.txt diff --git a/sandbox/assignment1/student3/file1.txt b/tests/sandbox/assignment1/student2/file1.txt similarity index 100% rename from sandbox/assignment1/student3/file1.txt rename to tests/sandbox/assignment1/student2/file1.txt diff --git a/sandbox/assignment1/student2/subdir/file2.txt b/tests/sandbox/assignment1/student2/subdir/file2.txt similarity index 100% rename from sandbox/assignment1/student2/subdir/file2.txt rename to tests/sandbox/assignment1/student2/subdir/file2.txt diff --git a/sandbox/issue28/student1/file1.txt b/tests/sandbox/assignment1/student3/file1.txt similarity index 100% rename from sandbox/issue28/student1/file1.txt rename to tests/sandbox/assignment1/student3/file1.txt diff --git a/sandbox/assignment1/student3/subdir/file2.txt b/tests/sandbox/assignment1/student3/subdir/file2.txt similarity index 100% rename from sandbox/assignment1/student3/subdir/file2.txt rename to tests/sandbox/assignment1/student3/subdir/file2.txt diff --git a/sandbox/issue28.json b/tests/sandbox/issue28.json similarity index 100% rename from sandbox/issue28.json rename to tests/sandbox/issue28.json diff --git a/sandbox/issue28/student3/file1.txt b/tests/sandbox/issue28/student1/file1.txt similarity index 100% rename from sandbox/issue28/student3/file1.txt rename to tests/sandbox/issue28/student1/file1.txt diff --git a/sandbox/issue28/student1/subdir/file2.txt b/tests/sandbox/issue28/student1/subdir/file2.txt similarity index 100% rename from sandbox/issue28/student1/subdir/file2.txt rename to tests/sandbox/issue28/student1/subdir/file2.txt diff --git a/sandbox/issue28/student2/subdir/file2.txt b/tests/sandbox/issue28/student2/subdir/file2.txt similarity index 100% rename from sandbox/issue28/student2/subdir/file2.txt rename to tests/sandbox/issue28/student2/subdir/file2.txt diff --git a/sandbox/issue28/student3/subdir/file2.txt b/tests/sandbox/issue28/student3/file1.txt similarity index 100% rename from sandbox/issue28/student3/subdir/file2.txt rename to tests/sandbox/issue28/student3/file1.txt diff --git a/tests/sandbox/issue28/student3/subdir/file2.txt b/tests/sandbox/issue28/student3/subdir/file2.txt new file mode 100644 index 0000000..e69de29 diff --git a/test_issue28.py b/tests/test_issue28.py similarity index 61% rename from test_issue28.py rename to tests/test_issue28.py index 038a266..c0f1277 100644 --- a/test_issue28.py +++ b/tests/test_issue28.py @@ -1,11 +1,25 @@ +# Copyright (C) 2014 Karl R. Wurst +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA import unittest -import sandbox import os.path import os from io import StringIO import sys -from assignment import Assignment -from command import Command +import tests.sandbox as sandbox +from submissions import Assignment, Command OUTFILE = sandbox.dir('issue28.txt') @@ -24,8 +38,6 @@ def test_missingFile_keepGoing(self): Assignment(sandbox.dir('issue28.json')).accept(collector.visit) self.assertTrue(len(collector.get_lines()) == 5) output = out.getvalue().strip() - self.assertRegex(output, 'Not found: .*student2/file1.txt', - msg=output) finally: self._restore_stdout() diff --git a/test_json.py b/tests/test_json.py similarity index 97% rename from test_json.py rename to tests/test_json.py index 2300c3b..925329f 100644 --- a/test_json.py +++ b/tests/test_json.py @@ -17,7 +17,7 @@ import json import os.path import os -import sandbox +import tests.sandbox as sandbox CONFIG_FILENAME = sandbox.dir('assignment1.json') diff --git a/test_command.py b/tests/test_submissions.py similarity index 55% rename from test_command.py rename to tests/test_submissions.py index a7bc8a6..590a2d5 100644 --- a/test_command.py +++ b/tests/test_submissions.py @@ -14,22 +14,57 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA import unittest -import sandbox -from command import Command +import os.path +import tests.sandbox as sandbox +from submissions import Assignment, Command from pathlib import Path +class test_Assignment(unittest.TestCase): + + def setUp(self): + self.collector = Collector() + + def test_each(self): + the_assignment = Assignment(sandbox.dir('assignment1.json')) + the_assignment.accept(self.collector.visit) + self.assertPathsExist() + + def assertPathsExist(self): + self.assertTrue( + self.collector.directories, msg="No directories collected.") + self.assertTrue(self.collector.files, msg="No files collected.") + for d in self.collector.directories: + self.assertTrue( + d.exists(), msg="Directory does not exist: " + str(d)) + self.assertTrue(d.is_dir(), msg="Not a directory: " + str(d)) + for f in self.collector.files: + self.assertTrue(f.exists(), msg="File does not exist: " + str(f)) + self.assertTrue(f.is_file(), msg="Not a file: " + str(f)) + + +class Collector(object): + def __init__(self): + self.directories = [] + self.files = [] + + def visit(self, submission_directory, files_to_collect): + self.directories.append(submission_directory) + for file in files_to_collect: + self.files.append(file) + + + + class test_Command(unittest.TestCase): def setUp(self): - Command.set_default_verbosity(False) self.directory = Path(sandbox.dir('test_Command')) self.directory.mkdir() self.file_ = self.directory / 'test_simple' self.filename = str(self.file_) def tearDown(self): - Command.set_default_verbosity(False) for f in self.directory.glob('*'): f.unlink() self.directory.rmdir() @@ -50,8 +85,8 @@ def test_implicit_ins(self): self.assertTrue(not self.file_.exists()) def _get_parameterized_commands(self): - touch = Command('touch "{ins}"') - remove = Command('rm "{ins}"') + touch = Command('touch {ins}') + remove = Command('rm {ins}') return (touch, remove) def test_explicit_ins(self): @@ -64,7 +99,7 @@ def test_explicit_ins(self): def test_list_ins(self): touch, remove = self._get_parameterized_commands() touch(ins=[self.file_, self.filename+'2']) - self.assertTrue(self.file_.exists()) + self.assertTrue(self.file_.exists(), msg="File: {}".format(self.file_)) self.assertTrue(Path(self.filename+'2').exists()) remove(ins=[self.file_, self.filename+'2']) self.assertTrue(not self.file_.exists()) @@ -73,40 +108,44 @@ def test_list_ins(self): def test_explicit_outs(self): touch, remove = self._get_parameterized_commands() touch(self.file_) - copy = Command('cat "{ins}" > "{outs}"') + copy = Command('cat {ins} > {outs}') copy(self.file_, outs=self.filename+'2') self.assertTrue(Path(self.filename+'2').exists()) def test_implicit_outs(self): touch, remove = self._get_parameterized_commands() touch(self.file_) - copy = Command('cat "{ins}" > "{outs}"') + copy = Command('cat {ins} > {outs}') copy(self.file_, self.filename+'2') self.assertTrue(Path(self.filename+'2').exists()) def test_only_outs(self): - write_hi = Command('echo hi > "{outs}"') + write_hi = Command('echo hi > {outs}') write_hi(outs=self.file_) self.assertTrue(self.file_.exists()) - def test_default_verbosity_is_false(self): - ls = Command('ls') - self.assertFalse(ls._verbose) - - def test_verbosity_true(self): - ls = Command('ls', verbose=True) - self.assertTrue(ls._verbose) - - def test_set_default_verbosity_true(self): - Command.set_default_verbosity(True) - ls = Command('ls') - self.assertTrue(ls._verbose) - - def test_override_default_verbosity_false(self): - Command.set_default_verbosity(True) - ls = Command('ls', False) - self.assertFalse(ls._verbose) - + def test_format_args_noneReturnsNone(self): + command = Command('') + self.assertIsNone(command._format_args(None)) + + def test_format_args_oneReturnsQuoted(self): + command = Command('') + result = command._format_args('hi') + self.assertEquals('"hi"', result) + + def test_format_args_quotesEscaped(self): + command = Command('') + original = '"hi"' + expected = '"\\"hi\\""' + result = command._format_args(original) + self.assertEquals(expected, result) + + def test_format_args_list(self): + command = Command('') + original = [ 'hi', 'mom', '"how"' ] + expected = '"hi" "mom" "\\"how\\""' + result = command._format_args(original) + self.assertEquals(expected, result) if __name__ == '__main__': unittest.main()