Files
TDC_R2C_Drawing/tests/test_r2c.py
T
eglee 2e4eca5e07 fix: reject DXF files missing EOF marker
Add regression coverage for issue #1.
2026-10-28 15:35:18 +09:00

266 lines
13 KiB
Python

import json
import io
import http.server
from urllib.parse import quote
import math
from collections import Counter
from pathlib import Path
import tempfile
import unittest
import r2c
class DrawingReconstructionTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.entities, cls.blocks, cls.raw = r2c.read_dxf(r2c.DEFAULT_DXF)
cls.model = r2c.extract_model(cls.entities, cls.raw)
cls.meshes = r2c.make_mesh(cls.model)
def test_diameter_and_dimensions_match_both_section_and_table(self):
m = self.model
self.assertEqual(m['diameter'], 1000)
self.assertAlmostEqual(m['length'], 1620)
self.assertAlmostEqual(m['width'], 1540)
self.assertAlmostEqual(m['height'], 1486)
self.assertAlmostEqual(m['axisHeight'], 666)
self.assertEqual(len(m['table']), 7)
self.assertEqual([r['D'] for r in m['table']], [700, 800, 900, 1000, 1100, 1200, 1350])
def test_inlet_is_rectangular_and_outlet_is_circular(self):
profiles = self.model['profiles']
self.assertEqual(profiles[0]['kind'], 'rectangle')
self.assertAlmostEqual(profiles[0]['halfWidth'] * 2, 1310)
self.assertAlmostEqual(profiles[0]['top'] + profiles[0]['bottom'], 1205)
self.assertEqual(profiles[2]['kind'], 'circle')
self.assertEqual(profiles[2]['radius'], 500)
self.assertEqual(profiles[2]['x'], 1400)
self.assertAlmostEqual(profiles[-1]['x'], 1620)
self.assertAlmostEqual(profiles[-1]['radius'], 615.8)
def test_pipe_is_actual_revolved_polyline(self):
self.assertEqual(self.model['evidence']['pipeProfile'], '195')
profile = self.model['pipeProfile']
self.assertAlmostEqual(max(p[0] for p in profile), 2620)
self.assertAlmostEqual(min(p[1] for p in profile), 500)
self.assertAlmostEqual(max(p[1] for p in profile), 666)
def test_closed_meshes_have_no_boundary_edges(self):
for name, mesh in self.meshes.items():
edges = Counter()
for face in mesh['faces']:
self.assertEqual(len(set(face)), len(face))
for a, b in zip(face, face[1:] + face[:1]):
edges[tuple(sorted((a, b)))] += 1
self.assertTrue(all(count == 2 for count in edges.values()), name)
self.assertTrue(all(math.isfinite(n) for p in mesh['vertices'] for n in p))
def test_r2c_bounds_match_native_drawing(self):
pts = self.meshes['r2c']['vertices']
for axis, expected in enumerate([1620, 1540, 1486]):
self.assertAlmostEqual(max(p[axis] for p in pts) - min(p[axis] for p in pts), expected, places=5)
def test_svg_preserves_curves_sections_and_korean_text(self):
svg, omitted = r2c.drawing_svg(self.entities, self.blocks)
self.assertIn('단면 A-A', svg)
self.assertIn('단면 B-B', svg)
self.assertIn('1,620', svg)
self.assertNotIn('%%C', svg)
self.assertGreater(svg.count('<polyline'), 800)
self.assertIn('HATCH', omitted)
def test_static_package_has_visible_geometry_without_javascript(self):
with tempfile.TemporaryDirectory() as directory:
m = r2c.build(output=directory, update_entry=False)
out = Path(directory)
source = (out / 'index.html').read_text()
self.assertNotIn('__INITIAL_SCENE__', source)
self.assertNotIn('__MODEL_JSON__', source)
self.assertNotIn('__DRAWING_SVG__', source)
self.assertNotIn('__VIEWER_JS__', source)
self.assertIn('function drawingRegions()', source)
scene = source.split('id="scene"', 1)[1].split('</svg>', 1)[0]
self.assertGreater(scene.count('<polygon'), 900)
self.assertTrue((out / 'drawing' / r2c.DEFAULT_DXF.name).is_file())
self.assertEqual(json.loads((out / 'model.json').read_text())['sha256'], m['sha256'])
self.assertTrue((out / 'model.obj').read_text().startswith('# R2C'))
def test_file_entry_redirects_to_configured_server_port(self):
with tempfile.TemporaryDirectory() as directory:
r2c.build(output=directory, update_entry=False, port=8080)
source = (Path(directory) / 'index.html').read_text()
self.assertNotIn('__SERVER_PORT__', source)
self.assertIn("if (location.protocol === 'file:')", source)
self.assertIn("location.replace('http://localhost:8080/' + location.search + location.hash)", source)
def test_web_handler_serves_generated_files_only(self):
class Handler(http.server.SimpleHTTPRequestHandler):
def log_message(self, *args):
pass
with tempfile.TemporaryDirectory() as directory:
r2c.build(output=directory, update_entry=False)
for path, status, content in [
('/index.html', b'200 OK', b'<polygon'),
('/model.obj', b'200 OK', b'# R2C'),
('/drawing/' + quote(r2c.DEFAULT_DXF.name), b'200 OK', b'SECTION'),
('/.git/config', b'404', b'File not found'),
]:
handler = object.__new__(Handler)
handler.directory = directory
handler.path = path
handler.command = 'GET'
handler.request_version = 'HTTP/1.1'
handler.requestline = 'GET ' + path + ' HTTP/1.1'
handler.headers = {}
handler.wfile = io.BytesIO()
handler.do_GET()
response = handler.wfile.getvalue()
self.assertIn(status, response.split(b'\r\n')[0])
self.assertIn(content, response)
def test_every_table_specification_has_its_own_closed_mesh(self):
for row in self.model['table']:
with self.subTest(diameter=row['D']):
variant = r2c.specification_model(self.model, row)
self.assertEqual(variant['dimensions'], row)
mesh = variant['meshes']['r2c']
for axis, key in enumerate(['L', 'B', 'H']):
self.assertAlmostEqual(max(p[axis] for p in mesh['vertices']) - min(p[axis] for p in mesh['vertices']), row[key], places=5)
self.assertAlmostEqual(variant['axisHeight'], row['h6'])
self.assertAlmostEqual(variant['profiles'][0]['halfWidth'] * 2, row['b3'])
self.assertAlmostEqual(variant['profiles'][2]['radius'] * 2, row['D'])
edges = Counter()
for face in mesh['faces']:
for a, b in zip(face, face[1:] + face[:1]):
edges[tuple(sorted((a, b)))] += 1
self.assertTrue(all(n == 2 for n in edges.values()))
self.assertTrue(all(math.isfinite(n) for point in mesh['vertices'] for n in point))
def test_drawn_specification_retains_exact_source_profiles(self):
v = r2c.specification_model(self.model, self.model['dimensions'])
self.assertEqual(v['basis'], 'drawn')
self.assertEqual(v['profiles'], self.model['profiles'])
self.assertEqual(v['meshes']['r2c'], self.meshes['r2c'])
self.assertEqual(v['warnings'], [])
def test_nominal_specifications_report_conflicting_socket_dimensions(self):
v = r2c.specification_model(self.model, self.model['table'][0])
self.assertEqual(v['basis'], 'table')
self.assertTrue(v['warnings'])
self.assertAlmostEqual(v['profiles'][3]['x'], v['length'] - v['dimensions']['L2'])
self.assertAlmostEqual(v['profiles'][-2]['x'], v['length'] - v['dimensions']['L3'])
self.assertNotAlmostEqual(v['length'] / self.model['length'], v['width'] / self.model['width'])
def test_invalid_table_is_rejected(self):
bad = dict(self.model['table'][0], B=100)
with self.assertRaises(ValueError):
r2c.specification_model(self.model, bad)
def api_request(self, raw, filename='upload.dxf', length=None, path='/api/convert'):
class Handler(r2c.ViewerHandler):
def log_message(self, *args):
pass
handler = object.__new__(Handler)
handler.path = path
handler.command = 'POST'
handler.request_version = 'HTTP/1.1'
handler.requestline = 'POST ' + path + ' HTTP/1.1'
handler.headers = {'Content-Length': str(len(raw) if length is None else length), 'X-Filename': quote(filename)}
handler.rfile = io.BytesIO(raw)
handler.wfile = io.BytesIO()
handler.do_POST()
header, body = handler.wfile.getvalue().split(b'\r\n\r\n', 1)
return int(header.split()[1]), json.loads(body)
def test_upload_converts_actual_file_and_provides_all_specs(self):
status, payload = self.api_request(self.raw, '첨부 도면.dxf')
self.assertEqual(status, 200)
self.assertEqual(payload['model']['source'], '첨부 도면.dxf')
self.assertEqual(len(payload['model']['variants']), 7)
self.assertEqual(payload['model']['sha256'], self.model['sha256'])
self.assertTrue(payload['drawingSvg'].startswith('<svg'))
self.assertEqual(r2c.DEFAULT_DXF.read_bytes(), self.raw)
def test_second_upload_uses_uploaded_table_instead_of_default_file(self):
changed = self.raw.replace(b'1,605', b'1,607')
self.assertNotEqual(changed, self.raw)
status, payload = self.api_request(changed, 'another.dxf')
self.assertEqual(status, 200)
self.assertEqual(payload['model']['variants'][0]['length'], 1607)
self.assertNotEqual(payload['model']['sha256'], self.model['sha256'])
self.assertEqual(r2c.DEFAULT_DXF.read_bytes(), self.raw)
def test_upload_rejects_invalid_oversized_or_truncated_input(self):
for raw, filename, length, expected in [
(b'', 'empty.dxf', None, 400),
(b'not a dxf', 'wrong.dxf', None, 422),
(b'AutoCAD Binary DXF', 'binary.dxf', None, 422),
(b'data', 'wrong.pdf', None, 415),
(b'x', 'large.dxf', r2c.MAX_UPLOAD_BYTES + 1, 413),
(b'x', 'missing.dxf', 10, 400),
]:
with self.subTest(expected=expected, filename=filename):
status, payload = self.api_request(raw, filename, length)
self.assertEqual(status, expected)
self.assertIn('error', payload)
status, payload = self.api_request(b'x', path='/wrong')
self.assertEqual(status, 404)
def test_inner_profiles_must_leave_positive_wall_thickness(self):
for changes in ({'D2': 5000}, {'D1': 5000}, {'b1': 5000},
{'h4': 5000}, {'D1': 10}):
with self.subTest(changes=changes):
row = dict(self.model['table'][0], **changes)
with self.assertRaisesRegex(ValueError, '관로 단면'):
r2c.specification_model(self.model, row)
def test_upload_rejects_dxf_missing_end_marker(self):
lines = self.raw.splitlines(keepends=True)
self.assertEqual(lines[-1].strip(), b'EOF')
truncated = b''.join(lines[:-2])
status, payload = self.api_request(truncated)
self.assertEqual(status, 422)
self.assertIn('EOF', payload['error'])
def test_spline_validation_and_valid_linear_interpolation(self):
def spline(weights=('1', '1'), knots=('0', '0', '1', '1'), degree='1'):
return {'type': 'SPLINE', 'tags': [
(71, degree), (10, '0'), (20, '0'), (10, '10'), (20, '20'),
*[(41, w) for w in weights], *[(40, k) for k in knots]]}
points = r2c.spline_points(spline())
self.assertEqual(points[0], [0, 0])
self.assertEqual(points[32], [5, 10])
self.assertEqual(points[-1], [10, 20])
for entity in (spline(weights=('0', '0')), spline(weights=('1',)),
spline(knots=('0', '1', '0', '1')),
spline(knots=('0', '0', '0', '0')), spline(degree='-1')):
with self.subTest(entity=entity), self.assertRaises(ValueError):
r2c.spline_points(entity)
def test_upload_with_invalid_spline_returns_json_error(self):
lines = self.raw.decode('utf-8-sig').splitlines()
start = next(i + 2 for i in range(0, len(lines), 2)
if lines[i].strip() == '2' and lines[i + 1].strip() == 'ENTITIES')
end = next(i for i in range(start, len(lines), 2)
if lines[i].strip() == '0' and lines[i + 1].strip() == 'ENDSEC')
extra = ('0 SPLINE 71 1 10 0 20 0 41 0 10 1 20 1 41 0 '
'40 0 40 0 40 1 40 1').split()
raw = '\n'.join(lines[:end] + extra + lines[end:]).encode('utf-8')
status, payload = self.api_request(raw)
self.assertEqual(status, 422)
self.assertIn('SPLINE', payload['error'])
def test_nonfinite_coordinates_are_rejected_before_geometry(self):
with self.assertRaisesRegex(ValueError, '유효하지 않은 좌표'):
r2c.read_dxf_bytes(b'10\nNaN\n')
def test_unrelated_drawing_does_not_produce_a_guessed_model(self):
with self.assertRaisesRegex(ValueError, '변화치수표'):
r2c.extract_model([], b'')
if __name__ == '__main__':
unittest.main()