diff --git a/README.md b/README.md index bd8960f..7d2d63c 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ TEST RESULT ``` Usage: treon - treon [PATH] [--threads=] [-v] [--exclude=]... + treon [PATH] [--threads=] [--html] [-v] [--exclude=]... Arguments: PATH File or directory path to find notebooks to test. Searches recursively for directory paths. [default: current working directory] @@ -73,6 +73,7 @@ Options: absolute path starts with the specified string are excluded from testing. This option can be specified more than once to exclude multiple files or directories. If the exclude path is a valid directory name, only this directory is excluded. + --html Write executed notebook to html -v --verbose Print detailed output for debugging. -h --help Show this screen. --version Show version. diff --git a/treon/task.py b/treon/task.py index ecce2b6..dc23549 100644 --- a/treon/task.py +++ b/treon/task.py @@ -14,15 +14,16 @@ def _is_verbose(): class Task: - def __init__(self, file_path): + def __init__(self, file_path, html): self.file_path = file_path self.is_successful = False + self.html = html def run_tests(self): LOG.info("Triggered test for %s", self.file_path) try: - self.is_successful, console_output = execute_notebook(self.file_path) + self.is_successful, console_output = execute_notebook(self.file_path, self.html) result = self.result_string() if not self.is_successful or _is_verbose(): diff --git a/treon/test_execution.py b/treon/test_execution.py index 468fa23..6cf18f8 100644 --- a/treon/test_execution.py +++ b/treon/test_execution.py @@ -1,16 +1,32 @@ +import logging import os import textwrap import nbformat from nbformat.v4 import new_code_cell -from nbconvert.preprocessors import ExecutePreprocessor +from nbconvert.preprocessors import ExecutePreprocessor, CellExecutionError +LOG = logging.getLogger('treon') -def execute_notebook(path): +def execute_notebook(path, html): notebook = nbformat.read(path, as_version=4) notebook.cells.extend([unittest_cell(), doctest_cell()]) processor = ExecutePreprocessor(timeout=-1, kernel_name='python3') - processor.preprocess(notebook, metadata(path)) + try: + processor.preprocess(notebook, metadata(path)) + except CellExecutionError: + if html: + # Write executed notebook to html + from nbconvert import HTMLExporter + html_exporter = HTMLExporter() + html_exporter.template_name = 'classic' + (body, resources) = html_exporter.from_notebook_node(notebook) + html_file_path = path + '.html' + with open(html_file_path, 'w', encoding='utf-8') as html_file: + LOG.info("Writing executed notebook to " + html_file_path) + html_file.write(body) + raise + return parse_test_result(notebook.cells) diff --git a/treon/treon.py b/treon/treon.py index 60c75f0..2b8e782 100644 --- a/treon/treon.py +++ b/treon/treon.py @@ -2,7 +2,7 @@ """ Usage: treon - treon [PATH] [--threads=] [-v] [--exclude=]... + treon [PATH] [--threads=] [--html] [-v] [--exclude=]... Arguments: PATH File or directory path to find notebooks to test. Searches recursively for directory paths. [default: current working directory] @@ -13,6 +13,7 @@ absolute path starts with the specified string are excluded from testing. This option can be specified more than once to exclude multiple files or directories. If the exclude path is a valid directory name, only this directory is excluded. + --html Write executed notebook to html -v --verbose Print detailed output for debugging. -h --help Show this screen. --version Show version. @@ -46,8 +47,9 @@ def main(): setup_logging(arguments) LOG.info('Executing treon version %s', __version__) thread_count = arguments['--threads'] or DEFAULT_THREAD_COUNT + html_output = arguments['--html'] or False notebooks = get_notebooks_to_test(arguments) - tasks = [Task(notebook) for notebook in notebooks] + tasks = [Task(notebook, html_output) for notebook in notebooks] print_test_collection(notebooks) trigger_tasks(tasks, thread_count) has_failed = print_test_result(tasks) @@ -65,7 +67,6 @@ def loglevel(arguments): verbose = arguments['--verbose'] return logging.DEBUG if verbose else logging.INFO - def trigger_tasks(tasks, thread_count): pool = ThreadPool(int(thread_count)) pool.map(Task.run_tests, tasks)