2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-30 20:46:47 +00:00
github-actions[bot] 2329179070
Parameter value bug (#7601) (#7602)
* Handle out-of range numerical values

* Add unit test

(cherry picked from commit 84d076848a901a1f40e636c6c593ca0004f1883e)

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2024-07-09 21:49:15 +10:00

414 lines
13 KiB
Python

"""Various unit tests for Part Parameters."""
import django.core.exceptions as django_exceptions
from django.test import TestCase, TransactionTestCase
from django.urls import reverse
from common.models import InvenTreeSetting
from InvenTree.unit_test import InvenTreeAPITestCase
from .models import (
Part,
PartCategory,
PartCategoryParameterTemplate,
PartParameter,
PartParameterTemplate,
)
class TestParams(TestCase):
"""Unit test class for testing the PartParameter model."""
fixtures = ['location', 'category', 'part', 'params']
def test_str(self):
"""Test the str representation of the PartParameterTemplate model."""
t1 = PartParameterTemplate.objects.get(pk=1)
self.assertEqual(str(t1), 'Length (mm)')
p1 = PartParameter.objects.get(pk=1)
self.assertEqual(str(p1), 'M2x4 LPHS : Length = 4 (mm)')
c1 = PartCategoryParameterTemplate.objects.get(pk=1)
self.assertEqual(str(c1), 'Mechanical | Length | 2.8')
def test_validate(self):
"""Test validation for part templates."""
n = PartParameterTemplate.objects.all().count()
t1 = PartParameterTemplate(name='abcde', units='dd')
t1.save()
self.assertEqual(n + 1, PartParameterTemplate.objects.all().count())
# Test that the case-insensitive name throws a ValidationError
with self.assertRaises(django_exceptions.ValidationError):
t3 = PartParameterTemplate(name='aBcde', units='dd')
t3.full_clean()
t3.save() # pragma: no cover
def test_invalid_numbers(self):
"""Test that invalid floating point numbers are correctly handled."""
p = Part.objects.first()
t = PartParameterTemplate.objects.create(name='Yaks')
valid_floats = ['-12', '1.234', '17', '3e45', '-12e34']
for value in valid_floats:
param = PartParameter(part=p, template=t, data=value)
param.full_clean()
self.assertIsNotNone(param.data_numeric)
invalid_floats = ['88E6352', 'inf', '-inf', 'nan', '3.14.15', '3eee3']
for value in invalid_floats:
param = PartParameter(part=p, template=t, data=value)
param.full_clean()
self.assertIsNone(param.data_numeric)
def test_metadata(self):
"""Unit tests for the metadata field."""
for model in [PartParameterTemplate]:
p = model.objects.first()
self.assertIsNone(p.get_metadata('test'))
self.assertEqual(p.get_metadata('test', backup_value=123), 123)
# Test update via the set_metadata() method
p.set_metadata('test', 3)
self.assertEqual(p.get_metadata('test'), 3)
for k in ['apple', 'banana', 'carrot', 'carrot', 'banana']:
p.set_metadata(k, k)
self.assertEqual(len(p.metadata.keys()), 4)
def test_get_parameter(self):
"""Test the Part.get_parameter method."""
prt = Part.objects.get(pk=3)
# Check that we can get a parameter by name
for name in ['Length', 'Width', 'Thickness']:
param = prt.get_parameter(name)
self.assertEqual(param.template.name, name)
# Check that an incorrect name returns None
param = prt.get_parameter('Not a parameter')
self.assertIsNone(param)
class TestCategoryTemplates(TransactionTestCase):
"""Test class for PartCategoryParameterTemplate model."""
fixtures = ['location', 'category', 'part', 'params']
def test_validate(self):
"""Test that category templates are correctly applied to Part instances."""
# Category templates
n = PartCategoryParameterTemplate.objects.all().count()
self.assertEqual(n, 2)
category = PartCategory.objects.get(pk=8)
t1 = PartParameterTemplate.objects.get(pk=2)
c1 = PartCategoryParameterTemplate(
category=category, parameter_template=t1, default_value='xyz'
)
c1.save()
n = PartCategoryParameterTemplate.objects.all().count()
self.assertEqual(n, 3)
class ParameterTests(TestCase):
"""Unit tests for parameter validation."""
fixtures = ['location', 'category', 'part', 'params']
def test_choice_validation(self):
"""Test that parameter choices are correctly validated."""
template = PartParameterTemplate.objects.create(
name='My Template',
description='A template with choices',
choices='red, blue, green',
)
pass_values = ['red', 'blue', 'green']
fail_values = ['rod', 'bleu', 'grene']
part = Part.objects.all().first()
for value in pass_values:
param = PartParameter(part=part, template=template, data=value)
param.full_clean()
for value in fail_values:
param = PartParameter(part=part, template=template, data=value)
with self.assertRaises(django_exceptions.ValidationError):
param.full_clean()
def test_unit_validation(self):
"""Test validation of 'units' field for PartParameterTemplate."""
# Test that valid units pass
for unit in [
None,
'',
'%',
'mm',
'A',
'm^2',
'Pa',
'V',
'C',
'F',
'uF',
'mF',
'millifarad',
]:
tmp = PartParameterTemplate(name='test', units=unit)
tmp.full_clean()
# Test that invalid units fail
for unit in ['mmmmm', '-', 'x', int]:
tmp = PartParameterTemplate(name='test', units=unit)
with self.assertRaises(django_exceptions.ValidationError):
tmp.full_clean()
def test_param_unit_validation(self):
"""Test that parameters are correctly validated against template units."""
template = PartParameterTemplate.objects.create(name='My Template', units='m')
prt = Part.objects.get(pk=1)
# Test that valid parameters pass
for value in [
'1',
'1m',
'm',
'-4m',
-2,
'2.032mm',
'99km',
'-12 mile',
'foot',
'3 yards',
]:
param = PartParameter(part=prt, template=template, data=value)
param.full_clean()
# Test that percent unit is working
template2 = PartParameterTemplate.objects.create(
name='My Template 2', units='%'
)
for value in ['1', '1%', '1 percent']:
param = PartParameter(part=prt, template=template2, data=value)
param.full_clean()
bad_values = ['3 Amps', '-3 zogs', '3.14F']
# Disable enforcing of part parameter units
InvenTreeSetting.set_setting(
'PART_PARAMETER_ENFORCE_UNITS', False, change_user=None
)
# Invalid units also pass, but will be converted to the template units
for value in bad_values:
param = PartParameter(part=prt, template=template, data=value)
param.full_clean()
# Enable enforcing of part parameter units
InvenTreeSetting.set_setting(
'PART_PARAMETER_ENFORCE_UNITS', True, change_user=None
)
for value in bad_values:
param = PartParameter(part=prt, template=template, data=value)
with self.assertRaises(django_exceptions.ValidationError):
param.full_clean()
def test_param_unit_conversion(self):
"""Test that parameters are correctly converted to template units."""
template = PartParameterTemplate.objects.create(name='My Template', units='m')
tests = {
'1': 1.0,
'-1': -1.0,
'23m': 23.0,
'-89mm': -0.089,
'100 foot': 30.48,
'-17 yards': -15.54,
}
prt = Part.objects.get(pk=1)
param = PartParameter(part=prt, template=template, data='1')
for value, expected in tests.items():
param.data = value
param.calculate_numeric_value()
self.assertAlmostEqual(param.data_numeric, expected, places=2)
class PartParameterTest(InvenTreeAPITestCase):
"""Tests for the ParParameter API."""
superuser = True
fixtures = ['category', 'part', 'location', 'params']
def test_list_params(self):
"""Test for listing part parameters."""
url = reverse('api-part-parameter-list')
response = self.get(url)
self.assertEqual(len(response.data), 7)
# Filter by part
response = self.get(url, {'part': 3})
self.assertEqual(len(response.data), 3)
# Filter by template
response = self.get(url, {'template': 1})
self.assertEqual(len(response.data), 4)
def test_param_template_validation(self):
"""Test that part parameter template validation routines work correctly."""
# Checkbox parameter cannot have "units" specified
with self.assertRaises(django_exceptions.ValidationError):
template = PartParameterTemplate(
name='test', description='My description', units='mm', checkbox=True
)
template.clean()
# Checkbox parameter cannot have "choices" specified
with self.assertRaises(django_exceptions.ValidationError):
template = PartParameterTemplate(
name='test',
description='My description',
choices='a,b,c',
checkbox=True,
)
template.clean()
# Choices must be 'unique'
with self.assertRaises(django_exceptions.ValidationError):
template = PartParameterTemplate(
name='test', description='My description', choices='a,a,b'
)
template.clean()
def test_create_param(self):
"""Test that we can create a param via the API."""
url = reverse('api-part-parameter-list')
response = self.post(url, {'part': '2', 'template': '3', 'data': 70})
self.assertEqual(response.status_code, 201)
response = self.get(url)
self.assertEqual(len(response.data), 8)
def test_param_detail(self):
"""Tests for the PartParameter detail endpoint."""
url = reverse('api-part-parameter-detail', kwargs={'pk': 5})
response = self.get(url)
self.assertEqual(response.status_code, 200)
data = response.data
self.assertEqual(data['pk'], 5)
self.assertEqual(data['part'], 3)
self.assertEqual(data['data'], '12')
# PATCH data back in
response = self.patch(url, {'data': '15'})
self.assertEqual(response.status_code, 200)
# Check that the data changed!
response = self.get(url)
data = response.data
self.assertEqual(data['data'], '15')
def test_order_parts_by_param(self):
"""Test that we can order parts by a specified parameter."""
def get_param_value(response, template, index):
"""Helper function to extract a parameter value from a response."""
params = response.data[index]['parameters']
for param in params:
if param['template'] == template:
return param['data']
# No match
return None
# Create a new parameter template
template = PartParameterTemplate.objects.create(
name='Test Template', description='My test template', units='m'
)
# Create parameters for each existing part
params = []
parts = Part.objects.all().order_by('pk')
for idx, part in enumerate(parts):
# Skip parts every now and then
if idx % 10 == 7:
continue
suffix = 'mm' if idx % 3 == 0 else 'm'
params.append(
PartParameter.objects.create(
part=part, template=template, data=f'{idx}{suffix}'
)
)
# Now, request parts, ordered by this parameter
url = reverse('api-part-list')
response = self.get(
url,
{'ordering': 'parameter_{pk}'.format(pk=template.pk), 'parameters': 'true'},
expected_code=200,
)
# All parts should be returned
self.assertEqual(len(response.data), len(parts))
# Check that the parts are ordered correctly (in increasing order)
expectation = {0: '0mm', 1: '3mm', 7: '4m', 9: '8m', -2: '13m', -1: None}
for idx, expected in expectation.items():
actual = get_param_value(response, template.pk, idx)
self.assertEqual(actual, expected)
# Next, check reverse ordering
response = self.get(
url,
{
'ordering': '-parameter_{pk}'.format(pk=template.pk),
'parameters': 'true',
},
expected_code=200,
)
expectation = {0: '13m', 1: '11m', -3: '3mm', -2: '0mm', -1: None}
for idx, expected in expectation.items():
actual = get_param_value(response, template.pk, idx)
self.assertEqual(actual, expected)