From 64bbd3ed6659d97b760ab55cce0cfe8b6be857c9 Mon Sep 17 00:00:00 2001 From: "jordi.peralta" Date: Wed, 24 Mar 2021 12:33:49 +0100 Subject: [PATCH] LITE-17919: Refactor Base Renderer --- .github/workflows/build.yml | 2 +- README.md | 2 +- connect/reports/renderers/base.py | 51 +++++++++++- connect/reports/renderers/csv.py | 46 ++--------- connect/reports/renderers/j2.py | 45 +---------- connect/reports/renderers/json.py | 43 +--------- connect/reports/renderers/pdf.py | 18 ++--- connect/reports/renderers/xlsx.py | 12 ++- tests/reports/renderers/test_base.py | 59 +++++++++++++- tests/reports/renderers/test_csv.py | 4 +- tests/reports/renderers/test_j2.py | 99 ++++-------------------- tests/reports/renderers/test_json.py | 44 +++-------- tests/reports/renderers/test_pdf.py | 24 +++--- tests/reports/renderers/test_registry.py | 2 +- tests/reports/renderers/test_xlsx.py | 44 ++--------- 15 files changed, 181 insertions(+), 314 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8e5016d..1098fe6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,4 @@ -name: Build Connect Reports Renderers +name: Build Connect Reports Core on: push: diff --git a/README.md b/README.md index fb750ad..867526a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Connect Reports Core -![pyversions](https://img.shields.io/pypi/pyversions/connect-reports-core.svg) [![Build Connect Reports Core](https://github.com/cloudblue/connect-reports-core/actions/workflows/build.yml/badge.svg)](https://github.com/cloudblue/connect-reports-core/actions/workflows/build.yml)[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=connect-reports-core&metric=alert_status)](https://sonarcloud.io/dashboard?id=connect-reports-core) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=connect-reports-core&metric=coverage)](https://sonarcloud.io/dashboard?id=connect-reports-core) [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=connect-reports-core&metric=sqale_rating)](https://sonarcloud.io/dashboard?id=connect-reports-core) +![pyversions](https://img.shields.io/pypi/pyversions/connect-reports-core.svg) [![PyPi Status](https://img.shields.io/pypi/v/connect-reports-core.svg)](https://pypi.org/project/connect-reports-core/) [![Build Connect Reports Core](https://github.com/cloudblue/connect-reports-core/actions/workflows/build.yml/badge.svg)](https://github.com/cloudblue/connect-reports-core/actions/workflows/build.yml) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=connect-reports-core&metric=alert_status)](https://sonarcloud.io/dashboard?id=connect-reports-core) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=connect-reports-core&metric=coverage)](https://sonarcloud.io/dashboard?id=connect-reports-core) [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=connect-reports-core&metric=sqale_rating)](https://sonarcloud.io/dashboard?id=connect-reports-core) ## Introduction diff --git a/connect/reports/renderers/base.py b/connect/reports/renderers/base.py index 41661f5..bd37c52 100644 --- a/connect/reports/renderers/base.py +++ b/connect/reports/renderers/base.py @@ -1,6 +1,13 @@ # Copyright © 2021 CloudBlue. All rights reserved. from abc import ABCMeta, abstractmethod +import zipfile +import os +from datetime import datetime +import tempfile +import json + +import pytz class BaseRenderer(metaclass=ABCMeta): @@ -34,9 +41,49 @@ def get_context(self, data): def set_extra_context(self, data): self.extra_context = data + def render(self, data, output_file, start_time=None): + start_time = start_time or datetime.now(tz=pytz.utc) + with tempfile.TemporaryDirectory() as tmpdir: + report_file = self.generate_report(data, f'{tmpdir}/report') + summary_file = self.generate_summary(f'{tmpdir}/summary', start_time) + pack_file = self.pack_files(report_file, summary_file, output_file) + return pack_file + + def generate_summary(self, output_file, start_time): + data = { + 'title': 'Report Execution Information', + 'data': { + 'report_start_time': start_time.isoformat(), + 'report_finish_time': datetime.now(tz=pytz.utc).isoformat(), + 'account_id': self.account.id, + 'account_name': self.account.name, + 'report_id': self.report.id, + 'report_name': self.report.name, + 'runtime_environment': self.environment, + 'report_execution_parameters': json.dumps( + self.report.values, + indent=4, + sort_keys=True, + ), + }, + } + output_file = f'{output_file}.json' + json.dump(data, open(output_file, 'w')) + return output_file + + def pack_files(self, report_file, summary_file, output_file): + tokens = output_file.split('.') + if tokens[-1] != 'zip': + output_file = f'{tokens[0]}.zip' + with zipfile.ZipFile(output_file, 'w') as repzip: + repzip.write(report_file, os.path.basename(report_file)) + repzip.write(summary_file, os.path.basename(summary_file)) + + return output_file + @abstractmethod - def render(self, data, output_file): - raise NotImplementedError('Subclasses must implement the `render` method.') + def generate_report(self, data, output_file): + raise NotImplementedError('Subclasses must implement the `generate_report` method.') @classmethod def validate(cls, definition): diff --git a/connect/reports/renderers/csv.py b/connect/reports/renderers/csv.py index 0468b8b..a749612 100644 --- a/connect/reports/renderers/csv.py +++ b/connect/reports/renderers/csv.py @@ -3,51 +3,17 @@ from connect.reports.renderers.base import BaseRenderer from connect.reports.renderers.registry import register -from datetime import datetime -import tempfile -import zipfile -import os import csv -import pytz - @register('csv') class CSVRenderer(BaseRenderer): - def render(self, data, output_file): - start_time = datetime.now(tz=pytz.utc) - - with tempfile.TemporaryDirectory() as tmpdir: - report_file = f'{tmpdir}/report.csv' - with open(report_file, 'w') as fp: - writer = csv.writer(fp, delimiter=';', quotechar='"', quoting=csv.QUOTE_ALL) - for row in data: - writer.writerow(row) - - summary_file = self._add_info_csv(start_time, f'{tmpdir}/summary.csv') - - output_file = f'{output_file}.zip' - with zipfile.ZipFile(output_file, 'w') as repzip: - repzip.write(report_file, os.path.basename(report_file)) - repzip.write(summary_file, os.path.basename(summary_file)) - - return output_file - - def _add_info_csv(self, start_time, output_file): - values = [] - values.append(('', 'Report Execution Information')) - values.append(('report_start_time', f'{start_time.isoformat()}')) - values.append(('report_finish_time', f'{datetime.now(tz=pytz.utc).isoformat()}')) - values.append(('account_id', f'{self.account.id}')) - values.append(('account_name', f'{self.account.name}')) - values.append(('report_id', f'{self.report.id}')) - values.append(('report_name', f'{self.report.name}')) - values.append(('runtime_environment', f'{self.environment}')) - values.append(('report_execution_parameters', f'{self.report.values}')) - + def generate_report(self, data, output_file): + tokens = output_file.split('.') + if tokens[-1] != 'csv': + output_file = f'{tokens[0]}.csv' with open(output_file, 'w') as fp: writer = csv.writer(fp, delimiter=';', quotechar='"', quoting=csv.QUOTE_ALL) - for value in values: - writer.writerow(value) - + for row in data: + writer.writerow(row) return output_file diff --git a/connect/reports/renderers/j2.py b/connect/reports/renderers/j2.py index 02a6d1f..8e5e777 100644 --- a/connect/reports/renderers/j2.py +++ b/connect/reports/renderers/j2.py @@ -1,12 +1,6 @@ # Copyright © 2021 CloudBlue. All rights reserved. import os -import json -from datetime import datetime -import tempfile -import zipfile - -import pytz from jinja2 import ( Environment, @@ -20,7 +14,7 @@ @register('jinja2') class Jinja2Renderer(BaseRenderer): - def render(self, data, output_file): + def generate_report(self, data, output_file): path, name = self.template.rsplit('/', 1) loader = FileSystemLoader(os.path.join(self.root_dir, path)) env = Environment( @@ -30,40 +24,9 @@ def render(self, data, output_file): template = env.get_template(name) _, ext, _ = name.rsplit('.', 2) - start_time = datetime.now(tz=pytz.utc) - with tempfile.TemporaryDirectory() as tmpdir: - report_file = f'{tmpdir}/report.{ext}' - template.stream(self.get_context(data)).dump(open(report_file, 'w')) - - summary_file = self._add_info_json(start_time, f'{tmpdir}/summary.json') - - output_file = f'{output_file}.zip' - with zipfile.ZipFile(output_file, 'w') as repzip: - repzip.write(report_file, os.path.basename(report_file)) - repzip.write(summary_file, os.path.basename(summary_file)) - - return output_file - - def _add_info_json(self, start_time, output_file): - data = { - 'title': 'Report Execution Information', - 'data': { - 'report_start_time': start_time.isoformat(), - 'report_finish_time': datetime.now(tz=pytz.utc).isoformat(), - 'account_id': self.account.id, - 'account_name': self.account.name, - 'report_id': self.report.id, - 'report_name': self.report.name, - 'runtime_environment': self.environment, - 'report_execution_parameters': json.dumps( - self.report.values, - indent=4, - sort_keys=True, - ), - }, - } - json.dump(data, open(output_file, 'w')) - return output_file + report_file = f'{output_file}.{ext}' + template.stream(self.get_context(data)).dump(open(report_file, 'w')) + return report_file @classmethod def validate(cls, definition): diff --git a/connect/reports/renderers/json.py b/connect/reports/renderers/json.py index d9eb965..2601555 100644 --- a/connect/reports/renderers/json.py +++ b/connect/reports/renderers/json.py @@ -3,49 +3,14 @@ from connect.reports.renderers.base import BaseRenderer from connect.reports.renderers.registry import register -from datetime import datetime import json -import tempfile -import zipfile -import os - -import pytz @register('json') class JSONRenderer(BaseRenderer): - def render(self, data, output_file): - start_time = datetime.now(tz=pytz.utc) - - with tempfile.TemporaryDirectory() as tmpdir: - report_file = f'{tmpdir}/report.json' - json.dump(data, open(report_file, 'w')) - summary_file = self._add_info_json(start_time, f'{tmpdir}/summary.json') - - output_file = f'{output_file}.zip' - with zipfile.ZipFile(output_file, 'w') as repzip: - repzip.write(report_file, os.path.basename(report_file)) - repzip.write(summary_file, os.path.basename(summary_file)) - - return output_file - - def _add_info_json(self, start_time, output_file): - data = { - 'title': 'Report Execution Information', - 'data': { - 'report_start_time': start_time.isoformat(), - 'report_finish_time': datetime.now(tz=pytz.utc).isoformat(), - 'account_id': self.account.id, - 'account_name': self.account.name, - 'report_id': self.report.id, - 'report_name': self.report.name, - 'runtime_environment': self.environment, - 'report_execution_parameters': json.dumps( - self.report.values, - indent=4, - sort_keys=True, - ), - }, - } + def generate_report(self, data, output_file): + tokens = output_file.split('.') + if tokens[-1] != 'json': + output_file = f'{tokens[0]}.json' json.dump(data, open(output_file, 'w')) return output_file diff --git a/connect/reports/renderers/pdf.py b/connect/reports/renderers/pdf.py index b51ddeb..169cd9e 100644 --- a/connect/reports/renderers/pdf.py +++ b/connect/reports/renderers/pdf.py @@ -1,7 +1,6 @@ # Copyright © 2021 CloudBlue. All rights reserved. import os -import shutil from functools import partial from weasyprint import HTML, default_url_fetcher @@ -30,24 +29,19 @@ def local_fetcher(url, root_dir=None, template_dir=None): @register('pdf') class PDFRenderer(Jinja2Renderer): - def render(self, data, output_file): - rendered_file = super().render(data, output_file) - temp_file = f'{output_file}.temp' - shutil.move( - rendered_file, - temp_file, - ) + def generate_report(self, data, output_file): + tokens = output_file.split('.') + if tokens[-1] != 'pdf': + output_file = f'{tokens[0]}.pdf' + rendered_file = super().generate_report(data, output_file) fetcher = partial( local_fetcher, root_dir=self.root_dir, template_dir=os.path.dirname(self.template), ) - - html = HTML(filename=temp_file, url_fetcher=fetcher) - output_file = f'{output_file}.pdf' + html = HTML(filename=rendered_file, url_fetcher=fetcher) html.write_pdf(output_file) - os.unlink(temp_file) return output_file @classmethod diff --git a/connect/reports/renderers/xlsx.py b/connect/reports/renderers/xlsx.py index a90326b..e98d4d5 100644 --- a/connect/reports/renderers/xlsx.py +++ b/connect/reports/renderers/xlsx.py @@ -19,8 +19,11 @@ @register('xlsx') class XLSXRenderer(BaseRenderer): - def render(self, data, output_file): - start_time = datetime.now(tz=pytz.utc) + def render(self, data, output_file, start_time): + self.start_time = start_time or datetime.now(tz=pytz.utc) + return self.generate_report(data, output_file) + + def generate_report(self, data, output_file): start_col_idx = self.args.get('start_col', 1) row_idx = self.args.get('start_row', 2) wb = load_workbook( @@ -29,13 +32,14 @@ def render(self, data, output_file): self.template, ), ) - ws = wb['Data'] for row in data: for col_idx, cell_value in enumerate(row, start=start_col_idx): ws.cell(row_idx, col_idx, value=cell_value) row_idx += 1 - self._add_info_sheet(wb.create_sheet('Info'), start_time) + + self._add_info_sheet(wb.create_sheet('Info'), self.start_time) + output_file = f'{output_file}.xlsx' wb.save(output_file) return output_file diff --git a/tests/reports/renderers/test_base.py b/tests/reports/renderers/test_base.py index b9cee68..4a609c7 100644 --- a/tests/reports/renderers/test_base.py +++ b/tests/reports/renderers/test_base.py @@ -2,9 +2,64 @@ import pytest +from fs.tempfs import TempFS + +from zipfile import ZipFile + from connect.reports.renderers.base import BaseRenderer +from connect.reports.datamodels import RendererDefinition -def test_base_render(): +def test_generate_report(): with pytest.raises(NotImplementedError): - BaseRenderer.render(None, None, None) + BaseRenderer.generate_report(None, None, None) + + +@pytest.mark.parametrize( + ('extra_context'), + ( + {'name': 'test', 'desc': 'description'}, + None, + ), +) +def test_render(account_factory, report_factory, report_data, extra_context): + + class DummyRenderer(BaseRenderer): + def generate_report(self, data, output_file): + output_file = f'{output_file}.ext' + open(output_file, 'w').write(str(data)) + return output_file + + tmp_fs = TempFS() + data = report_data(2, 2) + renderer = DummyRenderer( + 'runtime', + tmp_fs.root_path, + account_factory(), + report_factory(), + ) + renderer.set_extra_context(extra_context) + ctx = renderer.get_context(data) + + output_file = renderer.render(data, f'{tmp_fs.root_path}/report') + + assert output_file == f'{tmp_fs.root_path}/report.zip' + with ZipFile(output_file) as repzip: + assert sorted(repzip.namelist()) == ['report.ext', 'summary.json'] + with repzip.open('report.ext') as repfile: + assert repfile.read().decode('utf-8') == str(data) + + if extra_context: + assert 'name' in ctx['extra_context'] + assert 'desc' in ctx['extra_context'] + + +def test_validate_ok(): + defs = RendererDefinition( + root_path='root_path', + id='renderer_id', + type='json', + description='description', + ) + + assert BaseRenderer.validate(defs) == [] diff --git a/tests/reports/renderers/test_csv.py b/tests/reports/renderers/test_csv.py index 86040e3..a9fec25 100644 --- a/tests/reports/renderers/test_csv.py +++ b/tests/reports/renderers/test_csv.py @@ -20,7 +20,7 @@ def test_validate_ok(): assert CSVRenderer.validate(defs) == [] -def test_render_tmpfs_ok(account_factory, report_factory): +def test_render(account_factory, report_factory): with TempFS() as tmp_fs: data = [['line1'], ['line2']] renderer = CSVRenderer( @@ -34,7 +34,7 @@ def test_render_tmpfs_ok(account_factory, report_factory): assert output_file == f'{tmp_fs.root_path}/report.zip' with ZipFile(output_file) as repzip: - assert sorted(repzip.namelist()) == ['report.csv', 'summary.csv'] + assert sorted(repzip.namelist()) == ['report.csv', 'summary.json'] with repzip.open('report.csv') as repfile: content = repfile.read().decode('utf-8').split() assert content[0] == f'"{data[0][0]}"' diff --git a/tests/reports/renderers/test_j2.py b/tests/reports/renderers/test_j2.py index 173d4c8..487a6dd 100644 --- a/tests/reports/renderers/test_j2.py +++ b/tests/reports/renderers/test_j2.py @@ -5,8 +5,6 @@ import pytest -from datetime import datetime -import json from zipfile import ZipFile import csv from io import TextIOWrapper @@ -68,69 +66,6 @@ def test_validate_template_invalid_name(mocker): ] -@pytest.mark.parametrize( - ('extra_context'), - ( - {'name': 'test', 'desc': 'description'}, - None, - ), -) -def test_render(mocker, account_factory, report_factory, report_data, extra_context): - account = account_factory() - report = report_factory() - fsloader = mocker.MagicMock() - sauto = mocker.MagicMock() - stream_mock = mocker.MagicMock() - template_mock = mocker.MagicMock() - template_mock.stream.return_value = stream_mock - env = mocker.MagicMock() - env.get_template.return_value = template_mock - mocked_open = mocker.mock_open() - mocker.patch('connect.reports.renderers.j2.open', mocked_open) - mocked_tmpdir = mocker.patch('connect.reports.renderers.j2.tempfile.TemporaryDirectory') - mocked_autoescape = mocker.patch( - 'connect.reports.renderers.j2.select_autoescape', - return_value=sauto, - ) - mocked_zipfile = mocker.patch('connect.reports.renderers.j2.zipfile.ZipFile') - mocked_loader = mocker.patch( - 'connect.reports.renderers.j2.FileSystemLoader', - return_value=fsloader, - ) - mocked_environment = mocker.patch( - 'connect.reports.renderers.j2.Environment', - return_value=env, - ) - - renderer = Jinja2Renderer( - 'runtime environment', 'root_dir', account, report, - 'report_root/template.csv.j2', - ) - - data = report_data() - renderer.set_extra_context(extra_context) - ctx = renderer.get_context(data) - - assert renderer.render(data, 'report') == 'report.zip' - if extra_context: - assert 'name' in ctx['extra_context'] - assert 'desc' in ctx['extra_context'] - - mocked_loader.assert_called_once_with('root_dir/report_root') - mocked_environment.assert_called_once_with( - loader=fsloader, - autoescape=sauto, - ) - env.get_template.assert_called_once_with('template.csv.j2') - template_mock.stream.assert_called_once_with(ctx) - stream_mock.dump.assert_called_once() - assert mocked_open.mock_calls[0].args[0].rsplit('/', 1)[1] == 'report.csv' - assert mocked_open.mock_calls[0].args[1] == 'w' - assert mocked_zipfile.call_count == 1 - assert mocked_tmpdir.call_count == 1 - assert mocked_autoescape.call_count == 1 - - def test_validate_tempfs_ok(): tmp_fs = TempFS() tmplate_filename = 'template.csv.j2' @@ -166,7 +101,14 @@ def test_validate_tempfs_template_invalid_name(): ] -def test_render_tempfs_ok(report_data, account_factory, report_factory): +@pytest.mark.parametrize( + ('extra_context'), + ( + {'name': 'test', 'desc': 'description'}, + None, + ), +) +def test_render_tempfs_ok(report_data, account_factory, report_factory, extra_context): tmp_fs = TempFS() template_filename = 'template.csv.j2' directory_structure = 'package/report' @@ -187,6 +129,9 @@ def test_render_tempfs_ok(report_data, account_factory, report_factory): # report_data method generates a matrix with this aspect: # [['row_0_col_0', 'row_0_col_1'], ['row_1_col_0', 'row_1_col_1']] data = report_data(2, 2) + renderer.set_extra_context(extra_context) + ctx = renderer.get_context(data) + path_to_output_file = f'{tmp_fs.root_path}/{directory_structure}/report' output_file = renderer.render(data, path_to_output_file) @@ -200,22 +145,6 @@ def test_render_tempfs_ok(report_data, account_factory, report_factory): assert data[0] == content[1] assert data[1] == content[2] - -def test_add_info_json(account_factory, report_factory): - tmp_fs = TempFS() - - values = [{'param_id': 'param_value'}] - report = report_factory(values=values) - start_time = datetime.utcnow() - - renderer = Jinja2Renderer( - 'runtime', - tmp_fs.root_path, - account_factory(), - report, - ) - output_file = renderer._add_info_json(start_time, f'{tmp_fs.root_path}/summary.json') - data = json.load(open(output_file, 'r')) - - assert output_file == f'{tmp_fs.root_path}/summary.json' - assert data['data']['report_start_time'] == start_time.isoformat() + if extra_context: + assert 'name' in ctx['extra_context'] + assert 'desc' in ctx['extra_context'] diff --git a/tests/reports/renderers/test_json.py b/tests/reports/renderers/test_json.py index 6f8d329..cab9ed1 100644 --- a/tests/reports/renderers/test_json.py +++ b/tests/reports/renderers/test_json.py @@ -22,51 +22,25 @@ def test_validate_ok(): assert JSONRenderer.validate(defs) == [] -def test_render(mocker, account_factory, report_factory, report_data): - mocked_open = mocker.mock_open() - mocker.patch('connect.reports.renderers.json.open', mocked_open) - mocked_dump = mocker.patch('connect.reports.renderers.json.json.dump') - mocked_tmpdir = mocker.patch('connect.reports.renderers.json.tempfile.TemporaryDirectory') - mocked_zipfile = mocker.patch('connect.reports.renderers.json.zipfile.ZipFile') - - data = report_data() - renderer = JSONRenderer( - 'runtime environment', - 'root_dir', - account_factory(), - report_factory(), - ) - output_file = renderer.render(data, 'report') - - assert output_file == 'report.zip' - assert mocked_dump.mock_calls[0].args[0] == data - assert mocked_open.mock_calls[0].args[0].rsplit('/', 1)[1] == 'report.json' - assert mocked_open.mock_calls[0].args[1] == 'w' - assert mocked_zipfile.call_count == 1 - assert mocked_tmpdir.call_count == 1 - - -def test_add_info_json(account_factory, report_factory): +def test_generate_summary(account_factory, report_factory): tmp_fs = TempFS() - - values = [{'param_id': 'param_value'}] - report = report_factory(values=values) - start_time = datetime.utcnow() - + account = account_factory() + report = report_factory() renderer = JSONRenderer( 'runtime', tmp_fs.root_path, - account_factory(), + account, report, ) - output_file = renderer._add_info_json(start_time, f'{tmp_fs.root_path}/summary.json') - data = json.load(open(output_file, 'r')) + output_file = renderer.generate_summary( + f'{tmp_fs.root_path}/summary', + start_time=datetime.now(), + ) assert output_file == f'{tmp_fs.root_path}/summary.json' - assert data['data']['report_start_time'] == start_time.isoformat() -def test_render_tmpfs_ok(account_factory, report_factory, report_data): +def test_render(account_factory, report_factory, report_data): tmp_fs = TempFS() data = report_data(2, 2) renderer = JSONRenderer( diff --git a/tests/reports/renderers/test_pdf.py b/tests/reports/renderers/test_pdf.py index 9f8995b..8ca9a84 100644 --- a/tests/reports/renderers/test_pdf.py +++ b/tests/reports/renderers/test_pdf.py @@ -4,6 +4,8 @@ from fs.tempfs import TempFS +from zipfile import ZipFile + from connect.reports.datamodels import RendererDefinition from connect.reports.renderers import PDFRenderer from connect.reports.renderers.pdf import local_fetcher @@ -57,14 +59,15 @@ def test_local_fetcher(mocker, url, expected_url): def_fetcher.assert_called_once_with(expected_url) -def test_render(mocker, account_factory, report_factory, report_data): - mocker.patch('connect.reports.renderers.pdf.Jinja2Renderer.render', return_value='report.html') - mocked_mv = mocker.patch('connect.reports.renderers.pdf.shutil.move') +def test_generate_report(mocker, account_factory, report_factory, report_data): + mocker.patch( + 'connect.reports.renderers.pdf.Jinja2Renderer.generate_report', + return_value='report.pdf.html', + ) html = mocker.MagicMock() mocked_html = mocker.patch('connect.reports.renderers.pdf.HTML', return_value=html) fetcher = mocker.MagicMock() mocked_partial = mocker.patch('connect.reports.renderers.pdf.partial', return_value=fetcher) - mocked_unlink = mocker.patch('connect.reports.renderers.pdf.os.unlink') renderer = PDFRenderer( 'runtime environment', 'root_dir', @@ -73,17 +76,15 @@ def test_render(mocker, account_factory, report_factory, report_data): template='report_dir/template.html.j2', ) data = report_data() - assert renderer.render(data, 'report') == 'report.pdf' + assert renderer.generate_report(data, 'report.pdf') == 'report.pdf' - mocked_mv.assert_called_once_with('report.html', 'report.temp') mocked_partial.assert_called_once_with( local_fetcher, root_dir='root_dir', template_dir='report_dir', ) - assert mocked_html.mock_calls[0].kwargs['filename'] == 'report.temp' + assert mocked_html.mock_calls[0].kwargs['filename'] == 'report.pdf.html' assert mocked_html.mock_calls[0].kwargs['url_fetcher'] == fetcher html.write_pdf.assert_called_once_with('report.pdf') - mocked_unlink.assert_called_once_with('report.temp') def test_validate_tmpfs_template_wrong_name(): @@ -148,5 +149,8 @@ def test_render_tmpfs_ok(report_data, account_factory, report_factory): path_to_output = f'{tmp_fs.root_path}/package/report/report' output_file = renderer.render(data, path_to_output) - assert output_file == f'{path_to_output}.pdf' - assert 'PDF Report' in str(open(output_file, 'rb').read()) + assert output_file == f'{path_to_output}.zip' + with ZipFile(output_file) as zip_file: + assert sorted(zip_file.namelist()) == ['report.pdf', 'summary.json'] + with zip_file.open('report.pdf', 'r') as fp: + assert 'PDF Report' in str(fp.read()) diff --git a/tests/reports/renderers/test_registry.py b/tests/reports/renderers/test_registry.py index cc8a87e..4a2b867 100644 --- a/tests/reports/renderers/test_registry.py +++ b/tests/reports/renderers/test_registry.py @@ -57,7 +57,7 @@ def test_get_renderer_class_not_found(registry): def test_get_renderer(registry, account_factory, report_factory): @register('test') class TestRenderer(BaseRenderer): - def render(self, data, output_file): + def generate_report(self, data, output_file): pass account = account_factory() diff --git a/tests/reports/renderers/test_xlsx.py b/tests/reports/renderers/test_xlsx.py index bac13e8..5fd820b 100644 --- a/tests/reports/renderers/test_xlsx.py +++ b/tests/reports/renderers/test_xlsx.py @@ -1,6 +1,5 @@ # Copyright © 2021 CloudBlue. All rights reserved. -import json from datetime import datetime import pytest @@ -79,7 +78,7 @@ def test_validate_invalid_args(mocker, args, error): assert XLSXRenderer.validate(defs) == [error] -def test_render(mocker, account_factory, report_factory, report_data): +def test_generate_report(mocker, account_factory, report_factory, report_data): data = report_data() expected_calls = [] @@ -88,11 +87,9 @@ def test_render(mocker, account_factory, report_factory, report_data): expected_calls.append(mocker.call(2 + i, 1 + j, value=f'row_{i}_col_{j}')) data_sheet = mocker.MagicMock() - info_sheet = mocker.MagicMock() wbmock = mocker.MagicMock() wbmock.__getitem__.side_effect = [data_sheet] - wbmock.create_sheet.side_effect = [info_sheet] mocked_load_wb = mocker.patch( 'connect.reports.renderers.xlsx.load_workbook', @@ -105,44 +102,13 @@ def test_render(mocker, account_factory, report_factory, report_data): 'runtime environment', 'root_path', acc, report, template='template.xlsx', ) - renderer._add_info_sheet = mocker.MagicMock() - - assert renderer.render(data, 'report') == 'report.xlsx' + renderer.generate_summary = mocker.MagicMock() + renderer.start_time = datetime.utcnow() + assert renderer.generate_report(data, 'report') == 'report.xlsx' mocked_load_wb.assert_called_once_with('root_path/template.xlsx') - data_sheet.cell.assert_has_calls(expected_calls) - wbmock.create_sheet.assert_called_once_with('Info') wbmock.save.assert_called_once_with('report.xlsx') - assert renderer._add_info_sheet.mock_calls[0].args[0] == info_sheet - - -def test_add_info_sheet(mocker, account_factory, report_factory): - acc = account_factory() - values = [{'param_id': 'param_value'}] - report = report_factory(values=values) - start_time = datetime.utcnow() - - renderer = XLSXRenderer( - 'runtime environment', 'root_path', acc, report, template='template.xlsx', - ) - mocked_cells = {} - - def get_item(k): - return mocked_cells.setdefault(k, mocker.MagicMock()) - - ws = mocker.MagicMock() - ws.__getitem__.side_effect = get_item - - renderer._add_info_sheet(ws, start_time) - - assert mocked_cells['B2'].value == start_time.isoformat() - assert mocked_cells['B4'].value == acc.id - assert mocked_cells['B5'].value == acc.name - assert mocked_cells['B6'].value == report.id - assert mocked_cells['B7'].value == report.name - assert mocked_cells['B8'].value == 'runtime environment' - assert json.loads(mocked_cells['B9'].value) == values def test_validate_template_not_valid(): @@ -184,7 +150,7 @@ def test_render_tmpfs_ok(account_factory, report_factory, report_data): data = report_data(2, 2) path_to_output = f'{tmp_fs.root_path}/package/report/report' - output_file = renderer.render(data, path_to_output) + output_file = renderer.render(data, path_to_output, start_time=datetime.now()) wb = load_workbook(output_file) ws = wb['Data']