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
4 changes: 4 additions & 0 deletions doc/changes/unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@
## Bugfix

* #840: Added `export` plugin installation within `dependency-update.yml`

## Feature

* #722: Added check in `workflow:generate` to compare the generated and existing content before writing out
55 changes: 40 additions & 15 deletions exasol/toolbox/util/workflows/workflow.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import difflib
from collections.abc import Mapping
from pathlib import Path
from typing import (
Expand Down Expand Up @@ -41,38 +42,62 @@
class Workflow(BaseModel):
model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True)

template_path: Path
output_path: Path
content: str

@classmethod
def load_from_template(
cls,
file_path: Path,
template_path: Path,
output_directory: Path,
github_template_dict: dict[str, Any],
patch_yaml: WorkflowCommentedMap | None = None,
):
with bound_contextvars(template_file_name=file_path.name):
logger.debug("Load workflow template: %s", file_path.name)
with bound_contextvars(template_file_name=template_path.name):
logger.debug("Load workflow template: %s", template_path.name)

if not file_path.exists():
raise FileNotFoundError(file_path)
if not template_path.exists():
raise FileNotFoundError(template_path)

try:
workflow_renderer = WorkflowRenderer(
github_template_dict=github_template_dict,
file_path=file_path,
file_path=template_path,
patch_yaml=patch_yaml,
)
workflow = workflow_renderer.render()
return cls(content=workflow)
return cls(
template_path=template_path,
output_path=output_directory / template_path.name,
content=workflow_renderer.render(),
)
except (YamlError, YamlKeyError) as ex:
raise ex
except Exception as ex:
# Wrap all other "non-special" exceptions
raise ValueError(f"Error rendering file: {file_path}") from ex
raise ValueError(f"Error rendering file: {template_path}") from ex

def compare_to_file(self) -> str:
existing_content = (
self.output_path.read_text().strip() if self.output_path.exists() else ""
)
generated_content = self.content.strip()

diff = difflib.unified_diff(
existing_content.splitlines(),
generated_content.splitlines(),
fromfile=f"existing: {self.output_path.name}",
tofile="generated",
lineterm="",
)
return "\n".join(diff)

def write_to_file(self, file_path: Path) -> None:
logger.info("Write workflow file %s", file_path.name)
file_path.write_text(self.content + "\n")
def write_to_file(self) -> None:
if self.compare_to_file() == "":
logger.debug("Skip up-to-date workflow file %s", self.output_path.name)
return
logger.info("Write workflow file %s", self.output_path.name)
self.output_path.write_text(self.content + "\n")


def _select_workflow_template(workflow_name: WorkflowChoice) -> Mapping[str, Path]:
Expand Down Expand Up @@ -119,12 +144,12 @@ def update_workflow(workflow_choice: WorkflowChoice, config: BaseConfig) -> None

try:
workflow = Workflow.load_from_template(
file_path=workflow_dict[workflow_name],
template_path=workflow_dict[workflow_name],
output_directory=config.github_workflow_directory,
github_template_dict=config.github_template_dict,
patch_yaml=patch_yaml,
)
file_path = config.github_workflow_directory / f"{workflow_name}.yml"
workflow.write_to_file(file_path=file_path)
workflow.write_to_file()
except YamlKeyError as ex:
raise InvalidWorkflowPatcherEntryError(
file_path=config.github_workflow_patcher_yaml, entry=ex.entry # type: ignore
Expand Down
73 changes: 66 additions & 7 deletions test/unit/util/workflows/workflow_test.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from inspect import cleandoc
from pathlib import Path
from unittest.mock import patch

import pytest
Expand Down Expand Up @@ -47,23 +48,78 @@ def test_works_as_expected(tmp_path, project_config):
input_file_path.write_text(content)

workflow = Workflow.load_from_template(
file_path=input_file_path,
template_path=input_file_path,
output_directory=tmp_path,
github_template_dict=project_config.github_template_dict,
)
output_file_path = tmp_path / f"{input_file_path.name}"
workflow.write_to_file(file_path=output_file_path)
assert workflow.template_path == input_file_path
assert workflow.output_path == output_file_path
workflow.write_to_file()

assert output_file_path.read_text() == cleandoc(expected_yaml) + "\n"

@staticmethod
def test_compare_to_file_accepts_matching_content(tmp_path):
content = "line 1\nline 2"
file_path = tmp_path / "workflow.yml"
file_path.write_text(f"\n{content}\n")

workflow = Workflow(
template_path=file_path,
output_path=file_path,
content=f"\n{content}\n",
)

assert workflow.compare_to_file() == ""

@staticmethod
def test_compare_to_file_reports_diff(tmp_path):
file_path = tmp_path / "workflow.yml"
file_path.write_text("line 1\nline 3\n")
workflow = Workflow(
template_path=file_path,
output_path=file_path,
content="line 1\nline 2",
)

diff = workflow.compare_to_file()

assert diff == (
f"--- existing: {file_path.name}\n"
"+++ generated\n"
"@@ -1,2 +1,2 @@\n"
" line 1\n"
"-line 3\n"
"+line 2"
)

@staticmethod
def test_write_to_file_skips_up_to_date_file(tmp_path):
file_path = tmp_path / "workflow.yml"
file_path.write_text("line 1\nline 2\n")
workflow = Workflow(
template_path=file_path,
output_path=file_path,
content="line 1\nline 2",
)

with patch.object(Path, "write_text") as write_text:
workflow.write_to_file()

write_text.assert_not_called()
assert file_path.read_text() == "line 1\nline 2\n"

@staticmethod
@pytest.mark.parametrize("template_path", WORKFLOW_TEMPLATE_OPTIONS.values())
def test_works_for_all_templates(tmp_path, project_config, template_path):
workflow = Workflow.load_from_template(
file_path=template_path,
template_path=template_path,
output_directory=tmp_path,
github_template_dict=project_config.github_template_dict,
)
file_path = tmp_path / f"{template_path.name}"
workflow.write_to_file(file_path=file_path)
workflow.write_to_file()

assert file_path.read_text() != ""

Expand All @@ -72,7 +128,8 @@ def test_fails_when_yaml_does_not_exist(tmp_path, project_config):
file_path = tmp_path / "test.yaml"
with pytest.raises(FileNotFoundError, match="test.yaml"):
Workflow.load_from_template(
file_path=file_path,
template_path=file_path,
output_directory=tmp_path,
github_template_dict=project_config.github_template_dict,
)

Expand All @@ -89,7 +146,8 @@ def test_raises_custom_exceptions(tmp_path, project_config, raised_exc):
):
with pytest.raises(raised_exc):
Workflow.load_from_template(
file_path=file_path,
template_path=file_path,
output_directory=tmp_path,
github_template_dict=project_config.github_template_dict,
)

Expand All @@ -103,7 +161,8 @@ def test_other_exceptions_raised_as_valuerror(tmp_path, project_config):
):
with pytest.raises(ValueError):
Workflow.load_from_template(
file_path=file_path,
template_path=file_path,
output_directory=tmp_path,
github_template_dict=project_config.github_template_dict,
)

Expand Down