Skip to content

Commit

Permalink
test: fix all unit-tests
Browse files Browse the repository at this point in the history
  • Loading branch information
boromir674 committed Oct 29, 2023
1 parent 716b654 commit ee897c7
Show file tree
Hide file tree
Showing 4 changed files with 212 additions and 142 deletions.
2 changes: 1 addition & 1 deletion src/artificial_artwork/_demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def create_algo_runner(

content_image, style_image = read_images(content_img_file, style_img_file)

load_pretrained_model_functions()
load_pretrained_model_functions() # ie import VGG ModelHandler implementation (to allow facility creating instances)
model_design = type('ModelDesign', (), {
'pretrained_model': ModelHandlerFacility.create('vgg'),
'network_design': NetworkDesign.from_default_vgg()
Expand Down
222 changes: 159 additions & 63 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import typing as t
import pytest

from artificial_artwork.pretrained_model import model_handler
Expand Down Expand Up @@ -125,52 +126,6 @@ def iterate(self):
return TestSubject


@pytest.fixture
def toy_model_data():
"""Create a toy Network and Load the Handlers Facility"""
import numpy as np
from artificial_artwork.pretrained_model import ModelHandlerFacility
from artificial_artwork.pre_trained_models.vgg import VggModelRoutines, VggModelHandler

from functools import reduce
model_layers = (
'conv1_1',
'relu1',
'maxpool1',
)
convo_w_weights_shape = (3, 3, 3, 4)

class ToyModelRoutines(VggModelRoutines):

def load_layers(self, file_path: str):
return {
'layers': [[
[[[[model_layers[0]], 'unused', [[
np.reshape(np.array([i for i in range(1, reduce(lambda i,j: i*j, convo_w_weights_shape)+1)], dtype=np.float32), convo_w_weights_shape),
np.array([5], dtype=np.float32)
]]]]],
[[[[model_layers[1]], 'unused', [['W', 'b']]]]],
[[[[model_layers[2]], 'unused', [['W', 'b']]]]],
]]
}


toy_model_routines = ToyModelRoutines()

@ModelHandlerFacility.factory.register_as_subclass('toy')
class ToyModelHandler(VggModelHandler):
def _load_model_layers(self):
return toy_model_routines.load_layers('')['layers'][0]

@property
def model_routines(self):
return toy_model_routines

return type('TMD', (), {
'expected_layers': model_layers,
})


@pytest.fixture
def toy_network_design():
# layers we pick to use for our Neural Network
Expand Down Expand Up @@ -253,21 +208,47 @@ def vgg_layers():

@pytest.fixture
def pre_trained_models_1(vgg_layers, toy_model_data, toy_network_design):
import typing as t
from numpy.typing import NDArray
from artificial_artwork.production_networks import NetworkDesign
from artificial_artwork.pretrained_model import ModelHandlerFacility

toy_layers_loader: t.Callable[..., NDArray] = toy_model_data[0]
pretrained_toy_model_layers: t.List[str] = toy_model_data[1]

# equip ModelHandlerFacility with the 'vgg' class implementation
from artificial_artwork.pre_trained_models import vgg
# equip ModelHandlerFacility with the 'toy' class implementation
from artificial_artwork.pre_trained_models.vgg import VggModelRoutines, VggModelHandler
class ToyModelRoutines(VggModelRoutines):
# override only critical operations integrating with Prod Pretrained Stored Layers/Weights
def load_layers(self, file_path: str):
return toy_layers_loader(file_path)

toy_model_routines = ToyModelRoutines()

@ModelHandlerFacility.factory.register_as_subclass('toy')
class ToyModelHandler(VggModelHandler):
def _load_model_layers(self):
return toy_model_routines.load_layers('')['layers'][0]

@property
def model_routines(self):
return toy_model_routines

return {
'vgg': type('NSTModel', (), {
'pretrained_model': type('PTM', (), {
'expected_layers': vgg_layers,
'id': 'vgg',
'handler': ModelHandlerFacility.create('vgg'),
}),
# Production Style Layers and Output (Content) Layer picked from vgg
'network_design': NetworkDesign.from_default_vgg()
}),
# 'vgg': type('NSTModel', (), {
# 'pretrained_model': type('PTM', (), {
# 'expected_layers': vgg_layers,
# 'id': 'vgg',
# 'handler': ModelHandlerFacility.create('vgg'),
# }),
# # Production Style Layers and Output (Content) Layer picked from vgg
# 'network_design': NetworkDesign.from_default_vgg()
# }),
'toy': type('NSTModel', (), {
'pretrained_model': type('PTM', (), {
'expected_layers': toy_model_data.expected_layers,
'expected_layers': pretrained_toy_model_layers, # t.List[str]
'id': 'toy',
'handler': ModelHandlerFacility.create('toy'),
}),
Expand All @@ -276,15 +257,130 @@ def pre_trained_models_1(vgg_layers, toy_model_data, toy_network_design):
toy_network_design.style_layers,
toy_network_design.output_layer,
)
}),
}

}),}
@pytest.fixture
def model(pre_trained_models_1):
import os
print(f"\n -- PROD IM MODEL: {PRODUCTION_IMAGE_MODEL}")
print(f"Selected Prod?: {os.path.isfile(PRODUCTION_IMAGE_MODEL)}")
return {
True: pre_trained_models_1['vgg'],
False: pre_trained_models_1['toy'],
}[os.path.isfile(PRODUCTION_IMAGE_MODEL)]

return pre_trained_models_1['toy']
# return {
# True: pre_trained_models_1['vgg'],
# False: pre_trained_models_1['toy'],
# }[os.path.isfile(PRODUCTION_IMAGE_MODEL)]



# CONSTANT DATA Representing Layers Information (ie weight values) of Toy Network
@pytest.fixture
def toy_model_data():
import numpy as np

from functools import reduce
# This data format emulates the format the production pretrained VGG layer
# IDs are stored in
model_layers = (
'conv1_1',
'relu1',
'maxpool1',
)
convo_w_weights_shape = (3, 3, 3, 4)

def load_layers(*args):
"""Load Layers of 3-layered Toy Neural Net, emulating prod VGG format.
It emulates what the production implementation (scipy.io.loadmat) does,
by returning an object following the same interface as the one returned
by scipy.io.loadmat, when called on the file storing the production
pretrained VGG model.
"""
# here we use pytest to emit some text, leveraging pytest, so that the test code using this fixture
# can somehow verify that the text appeared in the expected place (ie console, log or sth)
print(f"VGG Mat Weights Mock Loader Called")

return {
'layers': [[
# 1st Layer: conv1_1
[[[[model_layers[0]], 'unused', [[
# 'A' Matrix weights tensor with shape (3, 3, 3, 4) (total nb of values = 3*3*3*4 = 108)
# for this toy Conv Layer we set the tensor values to be 1, 2, 3, ... 3 * 3 * 3 * 4 + 1 = 109
np.reshape(np.array([i for i in range(1, reduce(lambda i,j: i*j, convo_w_weights_shape)+1)], dtype=np.float32), convo_w_weights_shape),
# 'b' bias vector, which here is an array of shape (1,)
# for this toy Conv Layer we set the bias value to be 5
np.array([5], dtype=np.float32)
]]]]],
# 2nd Layer: relu1
[[[[model_layers[1]], 'unused', [['W', 'b']]]]], # these layer weights are not expected to be used, because the layer is not a Conv layer
# 3rd Layer: maxpool1
[[[[model_layers[2]], 'unused', [['W', 'b']]]]], # these layer weights are not expected to be used, because the layer is not a Conv layer
]]
}

return load_layers, model_layers


# MONKEYPATH PROD NST ALGO at RUNTIME with Algo using Toy Network
@pytest.fixture
def toy_nst_algorithm(toy_model_data, toy_network_design, monkeypatch):
from numpy.typing import NDArray

toy_layers_loader: t.Callable[..., NDArray] = toy_model_data[0]
# pretrained_toy_model_layer_ids: t.List[str] = toy_model_data[1]

def _monkeypatch():

return_toy_layers, _ = toy_model_data
from artificial_artwork.production_networks import NetworkDesign
from artificial_artwork.pretrained_model import ModelHandlerFacility
# equip Handler Facility Facory with the 'vgg' implementation
from artificial_artwork.pre_trained_models import vgg
import scipy.io

# if prod VGG Handler tries to load VGG Prod Weights, return Toy Weights instead
# 1st we patch the scipy.io.loadmat, which is used by the production VGG Handler
monkeypatch.setattr(scipy.io, 'loadmat', return_toy_layers) # Patch/replace-with-mock

from artificial_artwork.pre_trained_models.vgg import VggModelRoutines, VggModelHandler
class ToyModelRoutines(VggModelRoutines):
# override only critical operations integrating with Prod Pretrained Stored Layers/Weights
def load_layers(self, file_path: str):
return toy_layers_loader(file_path)

toy_model_routines = ToyModelRoutines()

class ToyModelHandler(VggModelHandler):
def _load_model_layers(self):
return toy_model_routines.load_layers('')['layers'][0]

@property
def model_routines(self):
return toy_model_routines

monkeypatch.setattr(
vgg, 'VggModelHandler', ToyModelHandler) # Patch/replace-with-mock

# 2nd we patch the AA_VGG_19 env var which the code strictly requires to find
import os
os.environ['AA_VGG_19'] = 'unit-tests-toy-value' # Patch/replace-with-mock

# Prod Code uses the 'default' factory (classmetod) method of class
# NetworkDesign, in order to instantiate a NetworkDesign object
# according to the 'Original' NST Algorithm (which layers to pick for
# creating ReLUs from their pretrained Conv A, b weights, or which is the Output Layer)

# Monkey patching objects used in the 'default' factory method
monkeypatch.setattr(NetworkDesign, 'from_default_vgg',
lambda: NetworkDesign(
toy_network_design.network_layers, # full list of layer IDs available in Pretrained Model
toy_network_design.style_layers, # list of tuples with layer IDs and coefficients governing their proportional contribution to the Style Cost/Loss formula
toy_network_design.output_layer, # layer ID to be used for Content Loss (ie last layer of Pretrained Model/Network)
)
)
# for convenience, construct here a ModelHanlder instance, equiped with
# handling all operations (of ModelHandlerInterface) with mocked Toy operations
# when needed and provide it to test code
# TODO remove the need for that
toy_model_handler = ModelHandlerFacility.create('vgg') # handler instances are stateless, and lightweight
return toy_model_handler
return _monkeypatch
57 changes: 50 additions & 7 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,40 @@


@pytest.mark.runner_setup(mix_stderr=False)
def test_cli_demo(test_suite, isolated_cli_runner, monkeypatch):
def test_cli_demo(test_suite, toy_nst_algorithm, isolated_cli_runner, monkeypatch):
"""Verify process exits with 0 after calling the CLI as `nst demo -it 4`.
Test that verifies the process exits with 0 when the CLI is invoked as
`nst demo -it 4`.
This means the NST receives as input Content and Style Images the 2 images
shipped with the Source Distribution for demoing purposes.
The NST is expected to iterate/learn (number of epochs to run) for 4 times.
The process is run in isolation, meaning that the process' stdout and
stderr are not mixed with the pytest's stdout and stderr.
"""
from pathlib import Path
from artificial_artwork.cli import entry_point as main
from artificial_artwork import _demo

# monkey patch _demo module to trick the _demo module in believing it is
# inside the Test Suite dir ('tests/'), so that it properly locates the demo
# Content and Style Images
monkeypatch.setattr(_demo, 'source_root_dir', Path(test_suite) / '..')

# Defer from using Production Pretrained Weights, and instead use the Toy Network
# That way this Test Case runs as a Unit Test, and does not need to integrate
# with the Production VGG Image Model.
# We achieve that by monkeypatching at runtime all the necessary objects, so that
# the program uses the Toy Network, which has but 1 Conv Layer (with very small
# dimensions too), with weights to use for the NST (as pretrained weights)
toy_nst_algorithm() # use fixture callable, which leverages monkeypatch under the hood

# Call CLI as `nst demo -it 4` in isolation
result = isolated_cli_runner.invoke(
main,
# args=['demo', '--help'],
args=['demo', '-it', '4'],
input=None,
env=None,
Expand All @@ -20,25 +46,42 @@ def test_cli_demo(test_suite, isolated_cli_runner, monkeypatch):
# **kwargs,
)
assert result.exit_code == 0
# GIVEN we can capture the stdout of the CLI (ie as a User would see if
# calling the CLI in an interactive shell)
assert type(result.stdout) == str

# WHEN we inspect the stdout of the CLI
string_to_inspect = result.stdout

# THEN we expect to see the following: VGG Mat Weights Mock Loader Called 1 time
# (ie the CLI called the VGG Mat Weights Mock Loader 1 time)
exp_str = 'VGG Mat Weights Mock Loader Called'

stdout_lines: t.List[str] = string_to_inspect.split('\n')
exp_str_appearances = stdout_lines.count(exp_str)
assert exp_str_appearances == 1



@pytest.mark.runner_setup(mix_stderr=False)
def test_cli_main(test_suite, isolated_cli_runner, monkeypatch):
def test_cli_main(test_suite,
toy_nst_algorithm, isolated_cli_runner):
from pathlib import Path
from artificial_artwork.cli import entry_point as main
from artificial_artwork import _demo
# monkeypatch.setattr(_demo, 'source_root_dir', Path(test_suite) / '..')

# Monkey Patch Prod NST (Prod Pretrained Weights) to use Toy Network (Toy Pretrained Weights)
toy_nst_algorithm() # use fixture callable, which leverages monkeypatch under the hood

result = isolated_cli_runner.invoke(
main,
# args=['demo', '--help'],
args=[
'run',
str(Path(test_suite) / 'data' / 'canoe_water_w300-h225.jpg'),
str(Path(test_suite) / 'data' / 'blue-red_w300-h225.jpg'),
'--iterations',
'6',
'--location', # output folder to store snapshots of Gen Image
'/tmp',
'/tmp', # TODO use os native pytest fixture for tempdir
],
input=None,
env=None,
Expand Down
Loading

0 comments on commit ee897c7

Please sign in to comment.