Skip to content

Commit

Permalink
refactor(UTILITIES): validation, file mixins
Browse files Browse the repository at this point in the history
  • Loading branch information
niall-byrne committed Feb 16, 2022
1 parent 1bd8bb5 commit cae4262
Show file tree
Hide file tree
Showing 27 changed files with 443 additions and 106 deletions.
2 changes: 2 additions & 0 deletions documentation/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@
"mac_maker.ansible_controller.tests",
"mac_maker.jobs.tests",
"mac_maker.utilities.tests",
"mac_maker.utilities.mixins.tests",
"mac_maker.utilities.validation.tests",
]

source_suffix = {
Expand Down
9 changes: 5 additions & 4 deletions mac_maker/ansible_controller/inventory.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
import os

from .. import config
from ..utilities.mixins.text_file import TextFileWriter
from ..utilities.state import TypeState


class InventoryFile:
class InventoryFile(TextFileWriter):
"""Inventory file for Ansible.
:param state: The loaded state content.
Expand All @@ -30,9 +31,9 @@ def write_inventory_file(self) -> None:
return

self._ensure_path_exists()

with open(self.state['inventory'], "w", encoding="utf-8") as fhandle:
fhandle.write(config.ANSIBLE_INVENTORY_CONTENT)
self.write_text_file(
config.ANSIBLE_INVENTORY_CONTENT, self.state['inventory']
)
self.log.debug(
"InventoryFile: inventory has been written to %s",
self.state['inventory'],
Expand Down
16 changes: 7 additions & 9 deletions mac_maker/ansible_controller/tests/test_inventory.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ def test_initialize(self) -> None:
self.assertEqual(self.inventory.state, self.loaded_state)

@mock.patch(INVENTORY_MODULE + ".os")
@mock.patch("builtins.open", new_callable=mock.mock_open)
@mock.patch(INVENTORY_MODULE + ".TextFileWriter.write_text_file")
def test_write_inventory_file(
self, m_open: mock.Mock, m_os: mock.Mock
self, m_write: mock.Mock, m_os: mock.Mock
) -> None:

m_os.path.exists.return_value = False
Expand All @@ -43,20 +43,18 @@ def test_write_inventory_file(
exist_ok=True,
)

m_open.assert_called_once_with(
self.loaded_state['inventory'], 'w', encoding="utf-8"
m_write.assert_called_once_with(
config.ANSIBLE_INVENTORY_CONTENT, self.loaded_state['inventory']
)
handle = m_open()
handle.write.assert_called_once_with(config.ANSIBLE_INVENTORY_CONTENT)

@mock.patch(INVENTORY_MODULE + ".os")
@mock.patch("builtins.open", new_callable=mock.mock_open)
@mock.patch(INVENTORY_MODULE + ".TextFileWriter.write_text_file")
def test_write_inventory_file_already_exists(
self, m_open: mock.Mock, m_os: mock.Mock
self, m_write: mock.Mock, m_os: mock.Mock
) -> None:
m_os.path.exists.return_value = True

self.inventory.write_inventory_file()

m_os.makedirs.assert_not_called()
m_open.assert_not_called()
m_write.assert_not_called()
9 changes: 6 additions & 3 deletions mac_maker/jobs/bases.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@
from ..ansible_controller.inventory import InventoryFile
from ..ansible_controller.runner import AnsibleRunner
from ..utilities.password import SUDO
from ..utilities.precheck import PrecheckConfig, TypePrecheckFileData
from ..utilities.spec import JobSpec
from ..utilities.state import TypeState
from ..utilities.validation.precheck import (
PrecheckConfigValidator,
TypePrecheckFileData,
)


class SimpleJobBase(abc.ABC):
Expand Down Expand Up @@ -42,8 +45,8 @@ def precheck(self) -> None:

precheck_data = self.get_precheck_content()

validator = PrecheckConfig(precheck_data['env'])
validator.is_valid_env_file()
validator = PrecheckConfigValidator(precheck_data['env'])
validator.validate_config()
results = validator.validate_environment()
if not results['is_valid']:
for violation in results['violations']:
Expand Down
2 changes: 1 addition & 1 deletion mac_maker/jobs/filesystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

import click
from .. import config
from ..utilities.precheck import TypePrecheckFileData
from ..utilities.state import TypeState
from ..utilities.validation.precheck import TypePrecheckFileData
from . import bases


Expand Down
2 changes: 1 addition & 1 deletion mac_maker/jobs/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
import click
from .. import config
from ..utilities.github import GithubRepository
from ..utilities.precheck import TypePrecheckFileData
from ..utilities.spec import TypeSpecFileData
from ..utilities.state import TypeState
from ..utilities.validation.precheck import TypePrecheckFileData
from ..utilities.workspace import WorkSpace
from . import bases

Expand Down
16 changes: 14 additions & 2 deletions mac_maker/jobs/tests/test_bases.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
from typing import cast
from unittest import TestCase, mock

from ...utilities import precheck, spec, state
from ...utilities import spec, state
from ...utilities.validation import precheck
from .. import bases as bases_module

BASES_MODULE = bases_module.__name__
Expand Down Expand Up @@ -37,7 +38,7 @@ def test_init(self) -> None:
)


@mock.patch(BASES_MODULE + ".PrecheckConfig")
@mock.patch(BASES_MODULE + ".PrecheckConfigValidator")
@mock.patch(BASES_MODULE + ".click.echo")
class TestJobsPrecheck(TestCase):
"""Test the ProvisionerJobBase class precheck method."""
Expand All @@ -64,6 +65,17 @@ def test_precheck_echo(self, m_echo: mock.Mock, m_env: mock.Mock) -> None:
self.concrete_job.mock_precheck_content['notes']
)

def test_precheck_config_invalid(
self,
_: mock.Mock,
m_env: mock.Mock,
) -> None:
instance = m_env.return_value
instance.validate_config.side_effect = precheck.ValidationError("Boom!")

with self.assertRaises(precheck.ValidationError):
self.concrete_job.precheck()

def test_precheck_environment(self, _: mock.Mock, m_env: mock.Mock) -> None:

instance = m_env.return_value
Expand Down
17 changes: 17 additions & 0 deletions mac_maker/tests/fixtures/fixtures_spec.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""Test harness for the Job Spec file test classes."""

import os
from pathlib import Path
from unittest import TestCase, mock

from ...utilities.mixins import json_file


class SpecFileTestHarness(TestCase):
"""Test harness for Job Spec file test classes."""

def setUp(self) -> None:
self.json_reader = json_file.JSONFileReader()
self.mock_spec_file_location = Path("spec.json")
self.mock_workspace = mock.Mock()
self.fixtures_folder = Path(os.path.dirname(__file__))
File renamed without changes.
File renamed without changes.
39 changes: 39 additions & 0 deletions mac_maker/utilities/mixins/json_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""JSONFile mixin classes."""

import json
from pathlib import Path
from typing import Any, Union


class JSONFileReader:
"""JSONFileReader mixin class."""

encoding = "utf-8"

def load_json_file(self, json_file_location: Union[Path, str]) -> Any:
"""Load a JSON file from the filesystem and return it as a Python object.
:param json_file_location: The path to the source file.
:returns: The loaded JSON object.
"""

with open(json_file_location, encoding=self.encoding) as fhandle:
json_file_content = json.load(fhandle)
return json_file_content


class JSONFileWriter:
"""JSONFileWriter mixin class."""

encoding = "utf-8"

def write_json_file(
self, python_object: Any, json_file_location: Union[Path, str]
) -> None:
"""Write a Python object to the filesystem as JSON.
:param python_object: The python object to write to file as JSON.
:param json_file_location: The path to the destination file.
"""
with open(json_file_location, "w", encoding=self.encoding) as file_handle:
json.dump(python_object, file_handle)
Empty file.
74 changes: 74 additions & 0 deletions mac_maker/utilities/mixins/tests/test_json_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"""Test the JSONFile mixin classes."""

import json
from unittest import TestCase, mock

from .. import json_file

JSON_FILE_MODULE = json_file.__name__


@mock.patch(JSON_FILE_MODULE + '.json')
@mock.patch('builtins.open')
class JSONFileReaderTest(TestCase):
"""Test the JSONFileReader mixin class."""

def setUp(self) -> None:
self.instance = json_file.JSONFileReader()
self.mock_object = {
"mock": "object"
}
self.mock_json = json.dumps(self.mock_object)
self.mock_context = mock.Mock()

def test_load_json_file_return_value(
self,
m_open: mock.Mock,
m_json: mock.Mock,
) -> None:
mock_path = "/mock/path"
self.mock_context.return_value = self.mock_json
m_open.return_value.__enter__.return_value = self.mock_context
m_json.load.return_value = self.mock_object

result = self.instance.load_json_file(mock_path)
self.assertEqual(result, self.mock_object)

def test_load_json_file_call(
self,
m_open: mock.Mock,
m_json: mock.Mock,
) -> None:
mock_path = "/mock/path"
m_open.return_value.__enter__.return_value = self.mock_context

self.instance.load_json_file(mock_path)

m_json.load.assert_called_once_with(self.mock_context)


@mock.patch(JSON_FILE_MODULE + '.json')
@mock.patch('builtins.open')
class JSONFileWriterTest(TestCase):
"""Test the JSONFileReader mixin class."""

def setUp(self) -> None:
self.instance = json_file.JSONFileWriter()
self.mock_object = {
"mock": "object"
}
self.mock_json = json.dumps(self.mock_object)
self.mock_context = mock.Mock()

def test_write_json_file_call(
self,
m_open: mock.Mock,
m_json: mock.Mock,
) -> None:
mock_path = "/mock/path"
self.mock_context.return_value = self.mock_json
m_open.return_value.__enter__.return_value = self.mock_context

self.instance.write_json_file(self.mock_object, mock_path)

m_json.dump.assert_called_once_with(self.mock_object, self.mock_context)
51 changes: 51 additions & 0 deletions mac_maker/utilities/mixins/tests/test_text_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""Test the TextFile mixin classes."""

from unittest import TestCase, mock

from .. import text_file

TEXT_FILE_MODULE = text_file.__name__


@mock.patch('builtins.open')
class TextFileReaderTest(TestCase):
"""Test the TextFileReader mixin class."""

def setUp(self) -> None:
self.instance = text_file.TextFileReader()
self.mock_context = mock.Mock()
self.mock_path = "/mock/path"
self.mock_text = "I am a mock string."

def test_read_text_file_return_value(self, m_open: mock.Mock) -> None:
m_open.return_value.__enter__.return_value.read.return_value = \
self.mock_text
result = self.instance.read_text_file(self.mock_path)
self.assertEqual(result, self.mock_text)

def test_read_text_file_call(self, m_open: mock.Mock) -> None:
self.instance.read_text_file(self.mock_path)
m_open.assert_called_once_with(
self.mock_path, encoding=text_file.TextFileReader.encoding
)
m_open.return_value.__enter__.return_value.read.assert_called_once_with()


@mock.patch('builtins.open')
class TextFileWriterTest(TestCase):
"""Test the TextFileWriter class."""

def setUp(self) -> None:
self.instance = text_file.TextFileWriter()
self.mock_context = mock.Mock()
self.mock_path = "/mock/path"
self.mock_text = "I am a mock string."

def test_write_text_file_call(self, m_open: mock.Mock) -> None:
self.instance.write_text_file(self.mock_text, self.mock_path)
m_open.assert_called_once_with(
self.mock_path, "w", encoding=text_file.TextFileReader.encoding
)
m_open.return_value.__enter__.return_value.write.assert_called_once_with(
self.mock_text
)
38 changes: 38 additions & 0 deletions mac_maker/utilities/mixins/text_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""TextFile mixin classes."""

from pathlib import Path
from typing import Union


class TextFileReader:
"""TextFileReader mixin class."""

encoding = "utf-8"

def read_text_file(self, text_file_location: Union[Path, str]) -> str:
"""Load a text file from the file system and return it as a string.
:param text_file_location: The path to the source file.
:returns: The loaded string object.
"""
with open(text_file_location, encoding=self.encoding) as file_handle:
data = file_handle.read()
return data


class TextFileWriter:
"""TextFileWriter mixin class."""

encoding = "utf-8"

def write_text_file(
self, text_file_content: str, text_file_location: Union[Path, str]
) -> None:
"""Load a text file from the file system and return it as a string.
:param text_file_content: The content to write to the file.
:param text_file_location: The path to the source file.
:returns: The loaded string object.
"""
with open(text_file_location, "w", encoding=self.encoding) as file_handle:
file_handle.write(text_file_content)
2 changes: 1 addition & 1 deletion mac_maker/utilities/spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
from jsonschema.validators import validator_for
from .. import config
from .filesystem import FileSystem
from .precheck import TypePrecheckFileData
from .state import State, TypeState
from .validation.precheck import TypePrecheckFileData
from .workspace import WorkSpace


Expand Down
Loading

0 comments on commit cae4262

Please sign in to comment.