Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 47 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ A **Job** in `abdiff` represents the A/B test for comparing the results from two

```json
{
"job_name": "<slugified version of passed job name>",
"transmogrifier_version_a": "<git commit SHA or tag name of version 'A' of Transmogrifier>",
"transmogrifier_version_b": "<git commit SHA or tag name of version 'B' of Transmogrifier>",
// any other data helpful to store about the job...
"job_directory": "amazing-job",
"job_message": "This job is testing all the things.",
"image_tag_a": "transmogrifier-example-job-1-abc123:latest",
"image_tag_b": "transmogrifier-example-job-1-def456:latest",
// potentially other job related data...
}
```

Expand All @@ -27,15 +28,13 @@ A `run.json` follows roughly the following format, demonstrating fields added by

```json
{
// all data from job.json included...,
"timestamp": "2024-08-23_15-55-00",
"transmogrifier_docker_image_a": "transmogrifier-job-<name>-version-a:latest",
"transmogrifier_docker_image_b": "transmogrifier-job-<name>-version-b:latest",
// all job data...
"timestamp": "2024-08-23_15-55-00",
"input_files": [
"s3://path/to/extract_file_1.xml",
"s3://path/to/extract_file_2.xml"
]
// any other data helpful to store about the run...
// potentially other run related data...
}
```

Expand Down Expand Up @@ -63,7 +62,45 @@ The following sketches a single job `"test-refactor"` and two runs `"2024-08-23_

## CLI commands

Coming soon...
### `abdiff`
```text
Usage: -c [OPTIONS] COMMAND [ARGS]...

Options:
-v, --verbose Pass to log at debug level instead of info.
-h, --help Show this message and exit.

Commands:
init-job Initialize a new Job.
ping Debug ping/pong command.
```

### `abdiff ping`
```text
Usage: -c ping [OPTIONS]

Debug ping/pong command.

Options:
-h, --help Show this message and exit.
```

### `abdiff init-job`
```
Usage: -c init-job [OPTIONS]

Initialize a new Job.

Options:
-m, --message TEXT Message to describe Job.
-d, --job-directory TEXT Job working directory to create. [required]
-a, --commit-sha-a TEXT Transmogrifier commit SHA for version 'A'
[required]
-b, --commit-sha-b TEXT Transmogrifier commit SHA for version 'B'
[required]
-h, --help Show this message and exit.
```


## Development

Expand Down
107 changes: 100 additions & 7 deletions abdiff/cli.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,120 @@
import json
import logging
from collections.abc import Callable
from datetime import timedelta
from time import perf_counter

import click
from click.exceptions import ClickException

from abdiff.config import configure_logger
from abdiff.core import build_ab_images
from abdiff.core import init_job as core_init_job
from abdiff.core.utils import read_job_json

logger = logging.getLogger(__name__)


@click.command()
@click.group(context_settings={"help_option_names": ["-h", "--help"]})
@click.option(
"-v", "--verbose", is_flag=True, help="Pass to log at debug level instead of info"
"-v",
"--verbose",
is_flag=True,
help="Pass to log at debug level instead of info.",
)
def main(*, verbose: bool) -> None:
start_time = perf_counter()
@click.pass_context
def main(
ctx: click.Context,
verbose: bool, # noqa: FBT001
) -> None:
ctx.ensure_object(dict)
ctx.obj["START_TIME"] = perf_counter()
root_logger = logging.getLogger()
logger.info(configure_logger(root_logger, verbose=verbose))
logger.info("Running process")

# Do things here!

elapsed_time = perf_counter() - start_time
@main.result_callback()
@click.pass_context
def post_main_group_subcommand(
ctx: click.Context,
*_args: tuple,
**_kwargs: dict,
) -> None:
"""Callback for any work to perform after a main sub-command completes."""
logger.info(
"Total time to complete process: %s", str(timedelta(seconds=elapsed_time))
"Total elapsed: %s",
str(
timedelta(seconds=perf_counter() - ctx.obj["START_TIME"]),
),
)


@main.command()
def ping() -> None:
"""Debug ping/pong command."""
logger.debug("got ping, preparing to pong")
click.echo("pong")


def shared_job_options(cli_command: Callable) -> Callable:
"""Decorator to provide shared CLI arguments to Job related commands."""
cli_command = click.option(
"-d",
"--job-directory",
type=str,
required=True,
help="Job working directory to create.",
)(cli_command)

cli_command = click.option(
"-m",
"--message",
type=str,
required=False,
help="Message to describe Job.",
default="Not provided.",
)(cli_command)

return cli_command # noqa: RET504


@main.command()
@shared_job_options
@click.option(
"-a",
"--commit-sha-a",
type=str,
required=True,
help="Transmogrifier commit SHA for version 'A'",
)
@click.option(
"-b",
"--commit-sha-b",
type=str,
required=True,
help="Transmogrifier commit SHA for version 'B'",
)
def init_job(
job_directory: str,
commit_sha_a: str,
commit_sha_b: str,
message: str,
) -> None:
"""Initialize a new Job."""
try:
core_init_job(job_directory, message)
except FileExistsError as exc:
message = (
f"Job directory already exists: '{job_directory}', cannot create new job."
)
raise ClickException(message) from exc

build_ab_images(
job_directory,
commit_sha_a,
commit_sha_b,
)

job_json = json.dumps(read_job_json(job_directory), indent=2)
logger.info(f"Job initialized: {job_json}")
81 changes: 73 additions & 8 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,82 @@
import os
from unittest.mock import patch

from abdiff.cli import main
from abdiff.core.utils import read_job_json


def test_cli_no_options(caplog, runner):
result = runner.invoke(main)
def test_cli_default_log_level_info(caplog, runner):
result = runner.invoke(main, ["ping"])
assert result.exit_code == 0
assert "Logger 'root' configured with level=INFO" in caplog.text
assert "Running process" in caplog.text
assert "Total time to complete process" in caplog.text
assert "pong" in result.output


def test_cli_all_options(caplog, runner):
result = runner.invoke(main, ["--verbose"])
def test_cli_verbose_sets_debug_log_level(caplog, runner):
caplog.set_level("DEBUG")
result = runner.invoke(main, ["--verbose", "ping"])
assert result.exit_code == 0
assert "Logger 'root' configured with level=DEBUG" in caplog.text
assert "Running process" in caplog.text
assert "Total time to complete process" in caplog.text
assert "got ping, preparing to pong" in caplog.text


def test_cli_main_group_callback_called(caplog, runner):
result = runner.invoke(main, ["--verbose", "ping"])
assert result.exit_code == 0
assert "Total elapsed" in caplog.text


@patch("abdiff.core.build_ab_images.docker_image_exists")
def test_init_job_all_arguments_success(
mocked_image_exists,
caplog,
runner,
job_directory,
):
mocked_image_exists.return_value = True
caplog.set_level("DEBUG")

message = "This is a Super Job."
_result = runner.invoke(
main,
[
"--verbose",
"init-job",
f"--job-directory={job_directory}",
f"--message={message}",
"--commit-sha-a=abc123",
"--commit-sha-b=def456",
],
)

assert os.path.exists(job_directory)

job_data = read_job_json(job_directory)
assert job_data == {
"job_directory": job_directory,
"job_message": message,
"image_tag_a": "transmogrifier-example-job-1-abc123:latest",
"image_tag_b": "transmogrifier-example-job-1-def456:latest",
}


def test_init_job_pre_existing_job_directory_raise_error(
caplog,
runner,
job_directory,
):
caplog.set_level("DEBUG")
os.makedirs(job_directory)
result = runner.invoke(
main,
[
"--verbose",
"init-job",
f"--job-directory={job_directory}",
"--message=This is a Super Job.",
"--commit-sha-a=abc123",
"--commit-sha-b=def456",
],
)
assert result.exit_code == 1
assert "Job directory already exists" in result.output