conservancy_beancount/tests/test_pdfforms_fields.py
2021-01-09 10:09:08 -05:00

378 lines
12 KiB
Python

"""test_pdfforms_fields.py - Unit tests for PDF forms manipulation"""
# Copyright © 2020 Brett Smith
# License: AGPLv3-or-later WITH Beancount-Plugin-Additional-Permission-1.0
#
# Full copyright and licensing details can be found at toplevel file
# LICENSE.txt in the repository.
import codecs
import itertools
import pytest
from decimal import Decimal
from pdfminer.psparser import PSLiteral
from conservancy_beancount.pdfforms import fields as fieldsmod
def field_source(
name=None,
value=None,
field_type=None,
flags=None,
parent=None,
kids=None,
*,
literal=None,
):
retval = {}
if isinstance(name, str):
retval['T'] = name.encode('ascii')
elif name is not None:
retval['T'] = name
if value is not None:
if literal is None:
literal = field_type and field_type != 'Tx'
if literal:
value = PSLiteral(value)
retval['V'] = value
if field_type is not None:
retval['FT'] = PSLiteral(field_type)
if flags is not None:
retval['Ff'] = flags
if parent is not None:
retval['Parent'] = parent
if kids is not None:
retval['Kids'] = list(kids)
return retval
def appearance_states(*names):
return {key: object() for key in names if key is not None}
def test_empty_field():
source = field_source()
field = fieldsmod.FormField(source)
assert not field.name()
assert field.value() is None
assert field.parent() is None
assert not list(field.kids())
assert field.flags() == 0
assert field.is_terminal()
with pytest.raises(ValueError):
field.field_type()
def test_text_field_base():
source = field_source(b's', b'string of text', 'Tx')
field = fieldsmod.FormField(source)
assert field.field_type() is fieldsmod.FieldType.TEXT
assert field.name() == 's'
assert field.value() == b'string of text'
@pytest.mark.parametrize('value', ['Off', 'Yes', 'On'])
def test_checkbox_field_base(value):
source = field_source(b'cb', value, 'Btn', literal=True)
field = fieldsmod.FormField(source)
assert field.field_type() is fieldsmod.FieldType.BUTTON
assert field.name() == 'cb'
assert field.value().name == value
@pytest.mark.parametrize('flags', range(4))
def test_readonly_flag(flags):
source = field_source(flags=flags)
field = fieldsmod.FormField(source)
assert field.flags() == flags
assert field.is_readonly() == flags % 2
@pytest.mark.parametrize('kid_count', range(3))
def test_kids(kid_count):
kids = [field_source(f'kid{n}', field_type='Ch') for n in range(kid_count)]
source = field_source(kids=iter(kids))
field = fieldsmod.FormField(source)
got_kids = list(field.kids())
assert len(got_kids) == len(kids)
assert field.is_terminal() == (not kids)
for actual, expected in zip(got_kids, kids):
assert actual.name() == expected['T'].decode('ascii')
def test_kids_by_type():
kids = [field_source(field_type='Tx'), field_source(field_type='Btn')]
source = field_source('topform', kids=iter(kids))
actual = fieldsmod.FormField.by_type(source).kids()
assert isinstance(next(actual), fieldsmod.TextField)
assert isinstance(next(actual), fieldsmod.CheckboxField)
assert next(actual, None) is None
def test_inheritance():
parent_source = field_source(b'parent', 'parent value', 'Tx', 17)
kid_source = field_source('kid', parent=parent_source)
parent_source['Kids'] = [kid_source]
field = fieldsmod.FormField(kid_source)
parent = field.parent()
assert parent is not None
assert parent.name() == 'parent'
assert not parent.is_terminal()
assert field.is_terminal()
assert field.name() == 'kid'
assert field.field_type() is fieldsmod.FieldType.TEXT
assert field.value() == 'parent value'
assert field.flags() == 17
assert not list(field.kids())
@pytest.mark.parametrize('field_type,value', [
('Tx', b'new value'),
('Btn', PSLiteral('Yes')),
])
def test_set_value(field_type, value):
source = field_source(field_type=field_type)
field = fieldsmod.FormField(source)
assert field.value() is None
field.set_value(value)
assert field.value() == value
@pytest.mark.parametrize('field_type,expected', [
('Tx', fieldsmod.TextField),
('Btn', fieldsmod.CheckboxField),
])
def test_by_type(field_type, expected):
source = field_source(field_type=field_type)
field = fieldsmod.FormField.by_type(source)
assert isinstance(field, expected)
def test_container_by_type():
kids = [field_source(field_type='Tx'), field_source(field_type='Btn')]
source = field_source('topform', kids=iter(kids))
field = fieldsmod.FormField.by_type(source)
assert isinstance(field, fieldsmod.FormField)
@pytest.mark.parametrize('flag', [
# If you add dedicated classes for these types of buttons, you can remove
# their test cases.
fieldsmod.FieldFlags.Radio,
fieldsmod.FieldFlags.Pushbutton,
])
def test_unsupported_button_by_type(flag):
source = field_source(field_type='Btn', flags=flag)
field = fieldsmod.FormField.by_type(source)
assert type(field) is fieldsmod.FormField
@pytest.mark.parametrize('field_type', [
# If you add dedicated classes for these types of fields, you can remove
# their test cases.
'Ch',
'Sig',
])
def test_unsupported_field_by_type(field_type):
source = field_source(field_type=field_type)
field = fieldsmod.FormField.by_type(source)
assert type(field) is fieldsmod.FormField
@pytest.mark.parametrize('value', [None, 'Off', 'Yes'])
def test_checkbox_value(value):
source = field_source('cb', value, 'Btn', literal=True)
field = fieldsmod.CheckboxField(source)
assert field.value() == (value and value == 'Yes')
@pytest.mark.parametrize('value,expected', [
(None, None),
(False, 'Off'),
(True, 'Yes'),
])
def test_checkbox_set_value(value, expected):
source = field_source('cb', field_type='Btn')
field = fieldsmod.CheckboxField(source)
field.set_value(value)
actual = fieldsmod.FormField.value(field)
if expected is None:
assert actual is None
else:
assert actual.name == expected
@pytest.mark.parametrize('on_key,off_key', itertools.product(
['1', '2', 'On', 'Yes'],
['Off', None],
))
def test_checkbox_options(on_key, off_key):
source = field_source('cb', field_type='Btn')
source['AP'] = {'N': appearance_states(on_key, off_key)}
field = fieldsmod.CheckboxField(source)
assert field.options() == [on_key, 'Off']
def test_checkbox_options_yes_no():
# I'm not sure this is actually allowed under the spec, but…
expected = ['Yes', 'No']
source = field_source('cb', field_type='Btn')
source['AP'] = {'N': appearance_states(*expected)}
field = fieldsmod.CheckboxField(source)
assert field.options() == expected
@pytest.mark.parametrize('on_key,off_key,set_value', itertools.product(
['1', '2', 'On', 'Yes'],
['Off', None],
[True, False, None],
))
def test_checkbox_set_custom_value(on_key, off_key, set_value):
source = field_source('cb', field_type='Btn')
source['AP'] = {'N': appearance_states(on_key, off_key)}
field = fieldsmod.CheckboxField(source)
field.set_value(set_value)
actual = fieldsmod.FormField.value(field)
if set_value is None:
assert actual is None
elif set_value:
assert actual.name == (on_key or 'Yes')
else:
assert actual.name == 'Off'
@pytest.mark.parametrize('encoding,prefix', [
('ascii', b''),
('utf-16be', codecs.BOM_UTF16_BE),
])
def test_text_value(encoding, prefix):
expected = f'{encoding} encoding test'
value = prefix + expected.encode(encoding)
source = field_source('t', value, 'Tx')
field = fieldsmod.TextField(source)
assert field.value() == expected
def test_text_value_none():
source = field_source(field_type='Tx')
assert fieldsmod.TextField(source).value() is None
@pytest.mark.parametrize('text,bprefix', [
('ASCII test', b''),
('UTF—16 test', codecs.BOM_UTF16_BE),
])
def test_text_set_value(text, bprefix):
source = field_source(field_type='Tx')
field = fieldsmod.TextField(source)
field.set_value(text)
assert field.value() == text
actual = fieldsmod.FormField.value(field)
assert actual == bprefix + text.encode('utf-16be' if bprefix else 'ascii')
@pytest.mark.parametrize('expected', [
'0',
'32',
'32.45',
'32,768',
'32,768.95',
])
def test_text_set_value_numeric(expected):
num_s = expected.replace(',', '')
field = fieldsmod.TextField({})
num_types = [Decimal, float]
if '.' not in expected:
num_types.append(int)
for num_type in num_types:
field.set_value(num_type(num_s))
assert field.value() == expected
field.set_value(None)
def test_text_set_value_none():
source = field_source('t', b'set None test', 'Tx')
field = fieldsmod.TextField(source)
field.set_value(None)
assert fieldsmod.FormField.value(field) is None
def test_empty_as_filled_fdf():
source = field_source()
field = fieldsmod.FormField(source)
assert field.as_filled_fdf() == {}
@pytest.mark.parametrize('field_type,field_class,set_value', [
('Btn', fieldsmod.CheckboxField, True),
('Btn', fieldsmod.CheckboxField, False),
('Ch', fieldsmod.FormField, None),
('Tx', fieldsmod.TextField, 'export test'),
('Tx', fieldsmod.TextField, 'UTF—16 export'),
])
def test_as_filled_fdf_after_set_value(field_type, field_class, set_value):
source = field_source(field_type, field_type=field_type)
field = field_class(source)
field.set_value(set_value)
actual = field.as_filled_fdf()
assert actual['T'] == field_type
expect_len = 2
if set_value is None:
assert 'V' not in actual
expect_len = 1
elif field_class is fieldsmod.CheckboxField:
assert actual['V'].name == ('Yes' if set_value else 'Off')
else:
assert actual['V'] == set_value
assert len(actual) == expect_len
@pytest.mark.parametrize('field_type,expected', [
('Btn', None),
('Tx', ''),
])
def test_as_filled_fdf_default_value(field_type, expected):
source = field_source(field_type=field_type)
field = fieldsmod.FormField.by_type(source)
actual = field.as_filled_fdf()
assert actual.get('V') == expected
def test_as_filled_fdf_recursion():
buttons = [field_source(f'bt{n}', field_type='Btn') for n in range(1, 3)]
pair = field_source('Buttons', kids=iter(buttons))
text = field_source('tx', field_type='Tx')
source = field_source('topform', kids=[text, pair])
field = fieldsmod.FormField(source)
actual = field.as_filled_fdf()
assert actual['T'] == 'topform'
assert 'V' not in actual
actual = iter(actual['Kids'])
assert next(actual)['T'] == 'tx'
actual = next(actual)
assert actual['T'] == 'Buttons'
assert 'V' not in actual
actual = iter(actual['Kids'])
assert next(actual)['T'] == 'bt1'
assert next(actual)['T'] == 'bt2'
assert next(actual, None) is None
@pytest.mark.parametrize('name,value,field_type', [
(None, None, None),
('mt', 'mapping text', 'Tx'),
('mb', 'Yes', 'Btn'),
])
def test_simple_as_mapping(name, value, field_type):
source = field_source(name, value, field_type)
field = fieldsmod.FormField(source)
actual = field.as_mapping()
key, mapped = next(actual)
assert key == (name or '')
assert mapped is field
assert next(actual, None) is None
def test_recursive_as_mapping():
btn_kids = [field_source(f'btn{n}', field_type='Btn') for n in range(1, 3)]
buttons = field_source('buttons', kids=iter(btn_kids))
text_kids = [field_source(f'tx{n}', field_type='Tx') for n in range(1, 3)]
texts = field_source('texts', kids=iter(text_kids))
source = field_source('root', kids=[texts, buttons])
root_field = fieldsmod.FormField(source)
actual = root_field.as_mapping()
for expected_key in [
'root',
'root.texts',
'root.texts.tx1',
'root.texts.tx2',
'root.buttons',
'root.buttons.btn1',
'root.buttons.btn2',
]:
key, field = next(actual)
assert key == expected_key
_, _, expected_name = expected_key.rpartition('.')
assert field.name() == expected_name
assert next(actual, None) is None
def test_add_kid():
parent = fieldsmod.FormField(field_source('parent'))
kid = fieldsmod.FormField(field_source('kid'))
parent.add_kid(kid)
actual, = parent.kids()
assert actual.name() == 'kid'
assert actual.parent().name() == 'parent'