Skip to content

Commit

Permalink
feat: add template-debugging options to openjd run (#52)
Browse files Browse the repository at this point in the history
Summary:
This adds two new debugging options to the `openjd run` command.
1. The new `--preserve` option to the `openjd run` command is added. When
   given, this option causes the runtime to retain the Session's Working
   Directory at completion of the run, rather than deleting it.
2. The new `--verbose` option sets the runtime's log level to DEBUG.
   This will cause the action-run logs to contain additional
   information, such as the contents of embedded files that it is saving
   to disk.

Signed-off-by: Daniel Neilson <53624638+ddneilson@users.noreply.github.com>
  • Loading branch information
ddneilson authored Feb 20, 2024
1 parent fc85b72 commit 80e8cb9
Show file tree
Hide file tree
Showing 5 changed files with 160 additions and 5 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ details on how Open Job Description's Jobs are run within Sessions.
|`--maximum-tasks`|integer|no| A maximum number of Tasks to run from this Step. Unless present, the Session will run all Tasks defined in the Step's parameter space or the Task(s) selected by the `--task-params` or `--tasks` arguments. Mutually exclusive with `--task-param/-tp` and `--tasks`. |`--maximum-tasks 5`|
|`--run-dependencies`|flag|no| If present, runs all of a Step's dependencies in the Session prior to the Step itself. |`--run-dependencies`|
|`--path-mapping-rules`|string, path|no| The path mapping rules to apply to the template. Should be a JSON-formatted list of Open Job Description path mapping rules, provided as a string or a path to a JSON/YAML document prefixed with 'file://'. |`--path-mapping-rules [{"source_os": "Windows", "source_path": "C:\test", "destination_path": "/mnt/test"}]`, `--path-mapping-rules file://rules_file.json`|
|`--preserve`|flag|no| If present, the Session's working directory will not be deleted when the run is completed. |`--preserve`|
|`--verbose`|flag|no| If present, then verbose logging will be enabled in the Session's log. |`--verbose`|
|`--output`|string|no| How to display the results of the command. Allowed values are `human-readable` (default), `json`, and `yaml`. |`--output json`, `--output yaml`|

#### Example
Expand Down
4 changes: 3 additions & 1 deletion src/openjd/cli/_common/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ def print_cli_result(command: Callable[[Namespace], OpenJDCliResult]) -> Callabl
Used to decorate the `do_<command>` functions for each command.
"""

def format_results(args: Namespace) -> None:
def format_results(args: Namespace) -> OpenJDCliResult:
response = command(args)

if args.output == "human-readable":
Expand All @@ -175,4 +175,6 @@ def format_results(args: Namespace) -> None:
if response.status == "error":
raise SystemExit(1)

return response

return format_results
2 changes: 2 additions & 0 deletions src/openjd/cli/_run/_local_session/_session_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ def __init__(
path_mapping_rules: Optional[list[PathMappingRule]] = None,
environments: Optional[list[EnvironmentTemplate]] = None,
should_print_logs: bool = True,
retain_working_dir: bool = False,
):
self.session_id = session_id
self.ended = Event()
Expand All @@ -85,6 +86,7 @@ def __init__(
job_parameter_values=job_parameters,
path_mapping_rules=self._path_mapping_rules,
callback=self._action_callback,
retain_working_dir=retain_working_dir,
)

# Initialize the action queue
Expand Down
32 changes: 29 additions & 3 deletions src/openjd/cli/_run/_run_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import json
from typing import Optional
import re
import logging

from ._local_session._session_manager import LocalSession, LogEntry
from .._common import (
Expand All @@ -22,7 +23,7 @@
Step,
StepParameterSpaceIterator,
)
from openjd.sessions import PathMappingRule
from openjd.sessions import PathMappingRule, LOG


@dataclass
Expand Down Expand Up @@ -119,6 +120,20 @@ def add_run_arguments(run_parser: ArgumentParser):
metavar="<path-to-JSON/YAML-file> [<path-to-JSON/YAML-file>] ...",
help="Apply the given environments to the Session in the order given.",
)
run_parser.add_argument(
"--preserve",
action="store_const",
const=True,
default=False,
help="Do not automatically delete the Session's Working Directory when complete.",
)
run_parser.add_argument(
"--verbose",
action="store_const",
const=True,
default=False,
help="Enable verbose logging while running the Session.",
)


def _collect_required_steps(step_map: dict[str, Step], step: Step) -> list[Step]:
Expand Down Expand Up @@ -301,6 +316,7 @@ def _run_local_session(
path_mapping_rules: Optional[list[PathMappingRule]],
should_run_dependencies: bool = False,
should_print_logs: bool = True,
retain_working_dir: bool = False,
) -> OpenJDCliResult:
"""
Creates a Session object and listens for log messages to synchronously end the session.
Expand All @@ -320,6 +336,7 @@ def _run_local_session(
path_mapping_rules=path_mapping_rules,
environments=environments,
should_print_logs=should_print_logs,
retain_working_dir=retain_working_dir,
) as session:
session.initialize(
dependencies=dependencies,
Expand All @@ -332,10 +349,15 @@ def _run_local_session(
# Monitor the local Session state
session.ended.wait()

preserved_message: str = ""
if retain_working_dir:
preserved_message = (
f"\nWorking directory preserved at: {str(session._inner_session.working_directory)}"
)
if session.failed:
return OpenJDRunResult(
status="error",
message="Session ended with errors; see Task logs for details",
message="Session ended with errors; see Task logs for details" + preserved_message,
job_name=job.name,
step_name=step.name,
duration=session.get_duration(),
Expand All @@ -345,7 +367,7 @@ def _run_local_session(

return OpenJDRunResult(
status="success",
message="Session ended successfully",
message="Session ended successfully" + preserved_message,
job_name=job.name,
step_name=step.name,
duration=session.get_duration(),
Expand Down Expand Up @@ -397,6 +419,9 @@ def do_run(args: Namespace) -> OpenJDCliResult:
rules_list = parsed_rules.get("path_mapping_rules")
path_mapping_rules = [PathMappingRule.from_dict(rule) for rule in rules_list]

if args.verbose:
LOG.setLevel(logging.DEBUG)

try:
# Raises: RuntimeError
the_job = generate_job(args)
Expand Down Expand Up @@ -430,4 +455,5 @@ def do_run(args: Namespace) -> OpenJDCliResult:
path_mapping_rules=path_mapping_rules,
should_run_dependencies=(args.run_dependencies),
should_print_logs=(args.output == "human-readable"),
retain_working_dir=args.preserve,
)
125 changes: 124 additions & 1 deletion test/openjd/cli/test_run_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import re
import os
from typing import Any, Optional
import logging

import pytest
from unittest.mock import Mock, patch
Expand All @@ -21,7 +22,7 @@
_validate_task_params,
)
from openjd.cli._run._local_session._session_manager import LocalSession
from openjd.sessions import PathMappingRule, PathFormat, Session
from openjd.sessions import LOG as SessionsLogger, PathMappingRule, PathFormat, Session
from openjd.model import decode_job_template, create_job, ParameterValue, ParameterValueType


Expand Down Expand Up @@ -283,6 +284,8 @@ def test_do_run_success(
path_mapping_rules=None,
environments=environments_files,
output="human-readable",
verbose=False,
preserve=False,
)

# WHEN
Expand All @@ -300,6 +303,120 @@ def test_do_run_success(
f.unlink()


def test_preserve_option(
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test that the 'run' command preserves the session working directory when asked to."""

files_created: list[Path] = []
try:
# GIVEN
with tempfile.NamedTemporaryFile(
mode="w+t", suffix=".template.json", encoding="utf8", delete=False
) as job_template_file:
json.dump(
{
"name": "TestJob",
"specificationVersion": "jobtemplate-2023-09",
"steps": [
{
"name": "TestStep",
"script": {
"actions": {"onRun": {"command": "echo", "args": ["Hello World"]}}
},
}
],
},
job_template_file.file,
)
files_created.append(Path(job_template_file.name))

args = Namespace(
path=Path(job_template_file.name),
step="TestStep",
job_params=[],
task_params=None,
tasks=None,
maximum_tasks=-1,
run_dependencies=False,
path_mapping_rules=None,
environments=[],
output="human-readable",
verbose=False,
preserve=True,
)

# WHEN
result = do_run(args)

# THEN
assert "Working directory preserved at" in result.message
# Extract the working directory from the output
match = re.search("Working directory preserved at: (.+)", result.message)
assert match is not None
dir = match[1]
assert Path(dir).exists()
finally:
for f in files_created:
f.unlink()


def test_verbose_option(
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test that the verbose option has set the log level of the openjd-sessions library to DEBUG."""

files_created: list[Path] = []
try:
# GIVEN
with tempfile.NamedTemporaryFile(
mode="w+t", suffix=".template.json", encoding="utf8", delete=False
) as job_template_file:
json.dump(
{
"name": "TestJob",
"specificationVersion": "jobtemplate-2023-09",
"steps": [
{
"name": "TestStep",
"script": {
"actions": {"onRun": {"command": "echo", "args": ["Hello World"]}}
},
}
],
},
job_template_file.file,
)
files_created.append(Path(job_template_file.name))

args = Namespace(
path=Path(job_template_file.name),
step="TestStep",
job_params=[],
task_params=None,
tasks=None,
maximum_tasks=-1,
run_dependencies=False,
path_mapping_rules=None,
environments=[],
output="human-readable",
verbose=True,
preserve=False,
)

# WHEN
do_run(args)

# THEN
assert SessionsLogger.isEnabledFor(logging.DEBUG)

# Reset the state to not interfere with other tests.
SessionsLogger.setLevel(logging.INFO)
finally:
for f in files_created:
f.unlink()


def test_do_run_error():
"""
Test that the `run` command exits on any error (e.g., a non-existent template file).
Expand All @@ -313,6 +430,8 @@ def test_do_run_error():
path_mapping_rules=None,
environments=[],
output="human-readable",
verbose=False,
preserve=False,
)
with pytest.raises(SystemExit):
do_run(mock_args)
Expand Down Expand Up @@ -371,6 +490,8 @@ def test_do_run_path_mapping_rules(caplog: pytest.LogCaptureFixture):
path_mapping_rules="file://" + temp_rules.name,
environments=[],
maximum_tasks=1,
verbose=False,
preserve=False,
)

# WHEN
Expand Down Expand Up @@ -415,6 +536,8 @@ def test_do_run_nonexistent_step(capsys: pytest.CaptureFixture):
path_mapping_rules=None,
environments=[],
output="human-readable",
verbose=False,
preserve=False,
)
with pytest.raises(SystemExit):
do_run(mock_args)
Expand Down

0 comments on commit 80e8cb9

Please sign in to comment.