6a3d64ff22
This is friendlier to the YAML input and consistent with FieldFlags. Less consistent with the rest of the codebase, but local consistency matters more IMO.
378 lines
12 KiB
Python
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'
|