Compare commits
13 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 56a99c8e58 | |||
| e72b792f6f | |||
| 286f9e1949 | |||
| 9e6f5a3365 | |||
| a67f9036b2 | |||
| 966e350f66 | |||
| 9cfcad9acd | |||
| 92492b01d4 | |||
| 49a7ba2209 | |||
| cf4d7f70d4 | |||
| 2512c83749 | |||
| 5cd20f9751 | |||
| 14f795e592 |
10 changed files with 535 additions and 33 deletions
|
|
@ -197,7 +197,7 @@ header = klamath.library.FileHeader.read(stream)
|
|||
struct_positions = klamath.library.scan_structs(stream)
|
||||
|
||||
stream.seek(struct_positions[b'my_struct'])
|
||||
elements_A = klamath.library.try_read_struct(stream)
|
||||
elements_A = klamath.library.read_elements(stream)
|
||||
|
||||
stream.close()
|
||||
|
||||
|
|
|
|||
|
|
@ -17,11 +17,6 @@ logger = logging.getLogger(__name__)
|
|||
class KlamathError(Exception):
|
||||
pass
|
||||
|
||||
try:
|
||||
from klamath_rs_ext import pack_int2, pack_int4
|
||||
_USE_RS_EXT = True
|
||||
except ImportError:
|
||||
_USE_RS_EXT = False
|
||||
|
||||
#
|
||||
# Parse functions
|
||||
|
|
@ -54,7 +49,7 @@ def decode_real8(nums: NDArray[numpy.uint64]) -> NDArray[numpy.float64]:
|
|||
exp = (nums >> 56) & 0x7f
|
||||
mant = (nums & 0x00ff_ffff_ffff_ffff).astype(numpy.float64)
|
||||
mant[neg != 0] *= -1
|
||||
return numpy.ldexp(mant, 4 * (exp - 64) - 56, signature=(float, int, float))
|
||||
return numpy.ldexp(mant, 4 * (exp.astype(numpy.int64) - 64) - 56)
|
||||
|
||||
|
||||
def parse_real8(data: bytes) -> NDArray[numpy.float64]:
|
||||
|
|
@ -82,7 +77,7 @@ def parse_datetime(data: bytes) -> list[datetime]:
|
|||
year, *date_parts = parse_int2(data[ii:ii + 12])
|
||||
try:
|
||||
dt = datetime(year + 1900, *date_parts)
|
||||
except ValueError as err:
|
||||
except ValueError:
|
||||
dt = datetime(1900, 1, 1, 0, 0, 0)
|
||||
logger.info(f'Invalid date {[year] + date_parts}, setting {dt} instead')
|
||||
dts.append(dt)
|
||||
|
|
@ -98,14 +93,14 @@ def pack_bitarray(data: int) -> bytes:
|
|||
return struct.pack('>H', data)
|
||||
|
||||
|
||||
def _pack_int2(data: NDArray[numpy.integer] | Sequence[int] | int) -> bytes:
|
||||
def pack_int2(data: NDArray[numpy.integer] | Sequence[int] | int) -> bytes:
|
||||
arr = numpy.asarray(data)
|
||||
if (arr > 32767).any() or (arr < -32768).any():
|
||||
raise KlamathError(f'int2 data out of range: {arr}')
|
||||
return arr.astype('>i2').tobytes()
|
||||
|
||||
|
||||
def _pack_int4(data: NDArray[numpy.integer] | Sequence[int] | int) -> bytes:
|
||||
def pack_int4(data: NDArray[numpy.integer] | Sequence[int] | int) -> bytes:
|
||||
arr = numpy.asarray(data)
|
||||
if (arr > 2147483647).any() or (arr < -2147483648).any():
|
||||
raise KlamathError(f'int4 data out of range: {arr}')
|
||||
|
|
@ -154,7 +149,7 @@ def encode_real8(fnums: NDArray[numpy.float64]) -> NDArray[numpy.uint64]:
|
|||
gds_exp = exp16 + 64
|
||||
|
||||
neg_biased = (gds_exp < 0)
|
||||
gds_mant[neg_biased] >>= (gds_exp[neg_biased] * 4).astype(numpy.uint16)
|
||||
gds_mant[neg_biased] >>= (-gds_exp[neg_biased] * 4).astype(numpy.uint16)
|
||||
gds_exp[neg_biased] = 0
|
||||
|
||||
too_big = (gds_exp > 0x7f) & ~(zero | subnorm)
|
||||
|
|
@ -165,7 +160,6 @@ def encode_real8(fnums: NDArray[numpy.float64]) -> NDArray[numpy.uint64]:
|
|||
|
||||
real8 = sign | gds_exp_bits | gds_mant
|
||||
real8[zero] = 0
|
||||
real8[gds_exp < -14] = 0 # number is too small
|
||||
|
||||
return real8.astype(numpy.uint64, copy=False)
|
||||
|
||||
|
|
@ -194,9 +188,3 @@ def read(stream: IO[bytes], size: int) -> bytes:
|
|||
if len(data) != size:
|
||||
raise EOFError
|
||||
return data
|
||||
|
||||
|
||||
if not _USE_RS_EXT:
|
||||
pack_int2 = _pack_int2
|
||||
pack_int4 = _pack_int4
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
Functionality for reading/writing elements (geometry, text labels,
|
||||
structure references) and associated properties.
|
||||
"""
|
||||
import io
|
||||
from typing import IO, TypeVar
|
||||
from collections.abc import Mapping
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
|
@ -53,6 +54,8 @@ def read_properties(stream: IO[bytes]) -> dict[int, bytes]:
|
|||
if key in properties:
|
||||
raise KlamathError(f'Duplicate property key: {key!r}')
|
||||
properties[key] = value
|
||||
else:
|
||||
stream.seek(size, io.SEEK_CUR)
|
||||
size, tag = Record.read_header(stream)
|
||||
return properties
|
||||
|
||||
|
|
@ -319,9 +322,9 @@ class Path(Element):
|
|||
if self.path_type == 4:
|
||||
bgn_ext, end_ext = self.extension
|
||||
if bgn_ext != 0:
|
||||
b += BGNEXTN.write(stream, bgn_ext)
|
||||
b += BGNEXTN.write(stream, int(bgn_ext))
|
||||
if end_ext != 0:
|
||||
b += ENDEXTN.write(stream, end_ext)
|
||||
b += ENDEXTN.write(stream, int(end_ext))
|
||||
b += XY.write(stream, self.xy)
|
||||
b += write_properties(stream, self.properties)
|
||||
b += ENDEL.write(stream, None)
|
||||
|
|
|
|||
|
|
@ -220,10 +220,15 @@ def scan_hierarchy(stream: IO[bytes]) -> dict[bytes, dict[bytes, int]]:
|
|||
colrow = COLROW.read_data(stream, size)
|
||||
ref_count = colrow[0] * colrow[1]
|
||||
elif tag == ENDEL.tag:
|
||||
if ref_count is None:
|
||||
ref_count = 1
|
||||
assert ref_name is not None
|
||||
cur_structure[ref_name] += ref_count
|
||||
if ref_name is not None:
|
||||
if ref_count is None:
|
||||
ref_count = 1
|
||||
cur_structure[ref_name] += ref_count
|
||||
ref_name = None
|
||||
ref_count = None
|
||||
elif tag in (SREF.tag, AREF.tag):
|
||||
ref_name = None
|
||||
ref_count = None
|
||||
else:
|
||||
stream.seek(size, io.SEEK_CUR)
|
||||
size, tag = Record.read_header(stream)
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ class HEADER(Int2Record):
|
|||
|
||||
class BGNLIB(DateTimeRecord):
|
||||
tag = 0x0102
|
||||
expected_size = 6 * 2
|
||||
expected_size = 2 * 6 * 2
|
||||
|
||||
|
||||
class LIBNAME(ASCIIRecord):
|
||||
|
|
@ -37,7 +37,7 @@ class ENDLIB(NoDataRecord):
|
|||
|
||||
class BGNSTR(DateTimeRecord):
|
||||
tag = 0x0502
|
||||
expected_size = 6 * 2
|
||||
expected_size = 2 * 6 * 2
|
||||
|
||||
|
||||
class STRNAME(ASCIIRecord):
|
||||
|
|
@ -173,6 +173,8 @@ class GENERATIONS(Int2Record):
|
|||
|
||||
@classmethod
|
||||
def check_data(cls: type[Self], data: NDArray[numpy.integer] | Sequence[int] | int) -> None:
|
||||
if isinstance(data, (int, numpy.integer)):
|
||||
return
|
||||
if not isinstance(data, Sized) or len(data) != 1:
|
||||
raise KlamathError(f'Expected exactly one integer, got {data}')
|
||||
|
||||
|
|
@ -222,7 +224,6 @@ class PROPATTR(Int2Record):
|
|||
|
||||
class PROPVALUE(ASCIIRecord):
|
||||
tag = 0x2c06
|
||||
expected_size = 2
|
||||
|
||||
|
||||
class BOX(NoDataRecord):
|
||||
|
|
@ -271,6 +272,8 @@ class FORMAT(Int2Record):
|
|||
|
||||
@classmethod
|
||||
def check_data(cls: type[Self], data: NDArray[numpy.integer] | Sequence[int] | int) -> None:
|
||||
if isinstance(data, (int, numpy.integer)):
|
||||
return
|
||||
if not isinstance(data, Sized) or len(data) != 1:
|
||||
raise KlamathError(f'Expected exactly one integer, got {data}')
|
||||
|
||||
|
|
@ -306,7 +309,7 @@ class SOFTFENCE(NoDataRecord):
|
|||
|
||||
|
||||
class HARDFENCE(NoDataRecord):
|
||||
tag = 0x3f00
|
||||
tag = 0x3e00
|
||||
|
||||
|
||||
class SOFTWIRE(NoDataRecord):
|
||||
|
|
|
|||
156
klamath/test_elements.py
Normal file
156
klamath/test_elements.py
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
import io
|
||||
import numpy
|
||||
from numpy.testing import assert_array_equal
|
||||
from klamath.elements import Boundary, Path, Text, Reference, Box, Node
|
||||
|
||||
def test_boundary_roundtrip() -> None:
|
||||
xy = numpy.array([[0, 0], [10, 0], [10, 10], [0, 10], [0, 0]], dtype=numpy.int32)
|
||||
b = Boundary(layer=(4, 5), xy=xy, properties={1: b'prop1'})
|
||||
|
||||
stream = io.BytesIO()
|
||||
b.write(stream)
|
||||
stream.seek(0)
|
||||
|
||||
b2 = Boundary.read(stream)
|
||||
assert b2.layer == b.layer
|
||||
assert_array_equal(b2.xy, b.xy)
|
||||
assert b2.properties == b.properties
|
||||
|
||||
def test_path_roundtrip() -> None:
|
||||
xy = numpy.array([[0, 0], [100, 0], [100, 100]], dtype=numpy.int32)
|
||||
p = Path(layer=(10, 20), xy=xy, properties={2: b'pathprop'},
|
||||
path_type=4, width=50, extension=(10, 20))
|
||||
|
||||
stream = io.BytesIO()
|
||||
p.write(stream)
|
||||
stream.seek(0)
|
||||
|
||||
p2 = Path.read(stream)
|
||||
assert p2.layer == p.layer
|
||||
assert_array_equal(p2.xy, p.xy)
|
||||
assert p2.properties == p.properties
|
||||
assert p2.path_type == p.path_type
|
||||
assert p2.width == p.width
|
||||
assert p2.extension == p.extension
|
||||
|
||||
def test_text_roundtrip() -> None:
|
||||
xy = numpy.array([[50, 50]], dtype=numpy.int32)
|
||||
t = Text(layer=(1, 1), xy=xy, string=b"HELLO WORLD", properties={},
|
||||
presentation=5, path_type=0, width=0, invert_y=True,
|
||||
mag=2.5, angle_deg=45.0)
|
||||
|
||||
stream = io.BytesIO()
|
||||
t.write(stream)
|
||||
stream.seek(0)
|
||||
|
||||
t2 = Text.read(stream)
|
||||
assert t2.layer == t.layer
|
||||
assert_array_equal(t2.xy, t.xy)
|
||||
assert t2.string == t.string
|
||||
assert t2.presentation == t.presentation
|
||||
assert t2.invert_y == t.invert_y
|
||||
assert t2.mag == t.mag
|
||||
assert t2.angle_deg == t.angle_deg
|
||||
|
||||
def test_reference_sref_roundtrip() -> None:
|
||||
xy = numpy.array([[100, 200]], dtype=numpy.int32)
|
||||
r = Reference(struct_name=b"MY_CELL", xy=xy, colrow=None,
|
||||
properties={5: b'sref'}, invert_y=False, mag=1.0, angle_deg=90.0)
|
||||
|
||||
stream = io.BytesIO()
|
||||
r.write(stream)
|
||||
stream.seek(0)
|
||||
|
||||
r2 = Reference.read(stream)
|
||||
assert r2.struct_name == r.struct_name
|
||||
assert_array_equal(r2.xy, r.xy)
|
||||
assert r2.colrow is None
|
||||
assert r2.properties == r.properties
|
||||
assert r2.angle_deg == r.angle_deg
|
||||
|
||||
def test_reference_aref_roundtrip() -> None:
|
||||
xy = numpy.array([[0, 0], [1000, 0], [0, 500]], dtype=numpy.int32)
|
||||
colrow = (5, 2)
|
||||
r = Reference(struct_name=b"ARRAY_CELL", xy=xy, colrow=colrow,
|
||||
properties={}, invert_y=False, mag=1.0, angle_deg=0.0)
|
||||
|
||||
stream = io.BytesIO()
|
||||
r.write(stream)
|
||||
stream.seek(0)
|
||||
|
||||
r2 = Reference.read(stream)
|
||||
assert r2.struct_name == r.struct_name
|
||||
assert_array_equal(r2.xy, r.xy)
|
||||
assert r2.colrow is not None
|
||||
assert list(r2.colrow) == list(colrow)
|
||||
assert r2.properties == r.properties
|
||||
|
||||
def test_box_roundtrip() -> None:
|
||||
xy = numpy.array([[0, 0], [10, 0], [10, 10], [0, 10], [0, 0]], dtype=numpy.int32)
|
||||
b = Box(layer=(30, 40), xy=xy, properties={})
|
||||
|
||||
stream = io.BytesIO()
|
||||
b.write(stream)
|
||||
stream.seek(0)
|
||||
|
||||
b2 = Box.read(stream)
|
||||
assert b2.layer == b.layer
|
||||
assert_array_equal(b2.xy, b.xy)
|
||||
|
||||
def test_node_roundtrip() -> None:
|
||||
xy = numpy.array([[0, 0], [10, 10]], dtype=numpy.int32)
|
||||
n = Node(layer=(50, 60), xy=xy, properties={})
|
||||
|
||||
stream = io.BytesIO()
|
||||
n.write(stream)
|
||||
stream.seek(0)
|
||||
|
||||
n2 = Node.read(stream)
|
||||
assert n2.layer == n.layer
|
||||
assert_array_equal(n2.xy, n.xy)
|
||||
|
||||
def test_reference_check() -> None:
|
||||
import pytest
|
||||
from klamath.basic import KlamathError
|
||||
# SREF with too many points
|
||||
xy = numpy.array([[0, 0], [10, 10]], dtype=numpy.int32)
|
||||
r = Reference(struct_name=b"CELL", xy=xy, colrow=None, properties={}, invert_y=False, mag=1.0, angle_deg=0.0)
|
||||
with pytest.raises(KlamathError, match="Expected size-2 xy"):
|
||||
r.check()
|
||||
|
||||
# AREF with too few points
|
||||
xy = numpy.array([[0, 0]], dtype=numpy.int32)
|
||||
r = Reference(struct_name=b"CELL", xy=xy, colrow=(2, 2), properties={}, invert_y=False, mag=1.0, angle_deg=0.0)
|
||||
with pytest.raises(KlamathError, match="colrow is not None, so expected size-6 xy"):
|
||||
r.check()
|
||||
|
||||
def test_read_properties_duplicate() -> None:
|
||||
import pytest
|
||||
from klamath.basic import KlamathError
|
||||
from klamath.records import PROPATTR, PROPVALUE, ENDEL
|
||||
stream = io.BytesIO()
|
||||
PROPATTR.write(stream, 1)
|
||||
PROPVALUE.write(stream, b"val1")
|
||||
PROPATTR.write(stream, 1) # DUPLICATE
|
||||
PROPVALUE.write(stream, b"val2")
|
||||
ENDEL.write(stream, None)
|
||||
stream.seek(0)
|
||||
|
||||
from klamath.elements import read_properties
|
||||
with pytest.raises(KlamathError, match="Duplicate property key"):
|
||||
read_properties(stream)
|
||||
|
||||
def test_element_read_unexpected_tag() -> None:
|
||||
import pytest
|
||||
from klamath.basic import KlamathError
|
||||
from klamath.records import SREF, SNAME, HEADER, XY, ENDEL
|
||||
stream = io.BytesIO()
|
||||
SREF.write(stream, None)
|
||||
SNAME.write(stream, b"CELL")
|
||||
HEADER.write(stream, 123) # UNEXPECTED TAG for Reference.read
|
||||
XY.write(stream, [0, 0])
|
||||
ENDEL.write(stream, None)
|
||||
stream.seek(0)
|
||||
|
||||
with pytest.raises(KlamathError, match="Unexpected tag"):
|
||||
Reference.read(stream)
|
||||
102
klamath/test_library.py
Normal file
102
klamath/test_library.py
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
import io
|
||||
import numpy
|
||||
from datetime import datetime
|
||||
from klamath.library import FileHeader, write_struct, try_read_struct, scan_structs, scan_hierarchy, read_elements
|
||||
from klamath.elements import Boundary
|
||||
from klamath.records import ENDLIB
|
||||
|
||||
def test_file_header_roundtrip() -> None:
|
||||
h = FileHeader(name=b"MY_LIB", user_units_per_db_unit=0.001, meters_per_db_unit=1e-9,
|
||||
mod_time=datetime(2023, 1, 1, 0, 0, 0), acc_time=datetime(2023, 1, 1, 0, 0, 0))
|
||||
|
||||
stream = io.BytesIO()
|
||||
h.write(stream)
|
||||
stream.seek(0)
|
||||
|
||||
h2 = FileHeader.read(stream)
|
||||
assert h2.name == h.name
|
||||
assert h2.user_units_per_db_unit == h.user_units_per_db_unit
|
||||
assert h2.meters_per_db_unit == h.meters_per_db_unit
|
||||
assert h2.mod_time == h.mod_time
|
||||
|
||||
def test_write_read_struct() -> None:
|
||||
xy = numpy.array([[0, 0], [10, 0], [10, 10], [0, 10], [0, 0]], dtype=numpy.int32)
|
||||
b = Boundary(layer=(1, 1), xy=xy, properties={})
|
||||
|
||||
stream = io.BytesIO()
|
||||
# Need a header for some operations, but write_struct works standalone
|
||||
write_struct(stream, name=b"CELL_A", elements=[b])
|
||||
ENDLIB.write(stream, None)
|
||||
stream.seek(0)
|
||||
|
||||
res = try_read_struct(stream)
|
||||
assert res is not None
|
||||
name, elements = res
|
||||
assert name == b"CELL_A"
|
||||
assert len(elements) == 1
|
||||
assert isinstance(elements[0], Boundary)
|
||||
|
||||
def test_scan_structs() -> None:
|
||||
stream = io.BytesIO()
|
||||
write_struct(stream, name=b"CELL_A", elements=[])
|
||||
write_struct(stream, name=b"CELL_B", elements=[])
|
||||
ENDLIB.write(stream, None)
|
||||
stream.seek(0)
|
||||
|
||||
positions = scan_structs(stream)
|
||||
assert b"CELL_A" in positions
|
||||
assert b"CELL_B" in positions
|
||||
|
||||
# Verify we can seek and read
|
||||
stream.seek(positions[b"CELL_B"])
|
||||
elements = read_elements(stream)
|
||||
assert len(elements) == 0
|
||||
|
||||
def test_scan_hierarchy() -> None:
|
||||
from klamath.elements import Reference
|
||||
|
||||
stream = io.BytesIO()
|
||||
# Struct A has 2 refs to Struct B
|
||||
ref_b1 = Reference(struct_name=b"B", xy=numpy.array([[0, 0]], dtype=numpy.int32), colrow=None, properties={},
|
||||
invert_y=False, mag=1.0, angle_deg=0.0)
|
||||
ref_b2 = Reference(struct_name=b"B", xy=numpy.array([[10, 10]], dtype=numpy.int32), colrow=None, properties={},
|
||||
invert_y=False, mag=1.0, angle_deg=0.0)
|
||||
write_struct(stream, name=b"A", elements=[ref_b1, ref_b2])
|
||||
|
||||
# Struct B has a 3x2 AREF of Struct C
|
||||
ref_c = Reference(struct_name=b"C", xy=numpy.array([[0, 0], [10, 0], [0, 10]], dtype=numpy.int32),
|
||||
colrow=(3, 2), properties={}, invert_y=False, mag=1.0, angle_deg=0.0)
|
||||
write_struct(stream, name=b"B", elements=[ref_c])
|
||||
|
||||
write_struct(stream, name=b"C", elements=[])
|
||||
ENDLIB.write(stream, None)
|
||||
stream.seek(0)
|
||||
|
||||
hierarchy = scan_hierarchy(stream)
|
||||
assert hierarchy[b"A"] == {b"B": 2}
|
||||
assert hierarchy[b"B"] == {b"C": 6}
|
||||
assert hierarchy[b"C"] == {}
|
||||
|
||||
def test_scan_structs_duplicate() -> None:
|
||||
import pytest
|
||||
from klamath.basic import KlamathError
|
||||
stream = io.BytesIO()
|
||||
write_struct(stream, name=b"CELL_A", elements=[])
|
||||
write_struct(stream, name=b"CELL_A", elements=[])
|
||||
ENDLIB.write(stream, None)
|
||||
stream.seek(0)
|
||||
|
||||
with pytest.raises(KlamathError, match="Duplicate structure name"):
|
||||
scan_structs(stream)
|
||||
|
||||
def test_scan_hierarchy_duplicate() -> None:
|
||||
import pytest
|
||||
from klamath.basic import KlamathError
|
||||
stream = io.BytesIO()
|
||||
write_struct(stream, name=b"CELL_A", elements=[])
|
||||
write_struct(stream, name=b"CELL_A", elements=[])
|
||||
ENDLIB.write(stream, None)
|
||||
stream.seek(0)
|
||||
|
||||
with pytest.raises(KlamathError, match="Duplicate structure name"):
|
||||
scan_hierarchy(stream)
|
||||
134
klamath/test_record.py
Normal file
134
klamath/test_record.py
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
import io
|
||||
import pytest
|
||||
import struct
|
||||
from datetime import datetime
|
||||
from klamath.basic import KlamathError
|
||||
from klamath.record import (
|
||||
write_record_header, read_record_header, expect_record,
|
||||
BitArrayRecord, Int2Record, ASCIIRecord, DateTimeRecord, NoDataRecord
|
||||
)
|
||||
from klamath.records import ENDLIB, HEADER
|
||||
|
||||
def test_write_read_record_header() -> None:
|
||||
stream = io.BytesIO()
|
||||
tag = 0x1234
|
||||
data_size = 8
|
||||
|
||||
write_record_header(stream, data_size, tag)
|
||||
stream.seek(0)
|
||||
|
||||
read_size, read_tag = read_record_header(stream)
|
||||
assert read_size == data_size
|
||||
assert read_tag == tag
|
||||
assert stream.tell() == 4
|
||||
|
||||
def test_write_record_header_too_big() -> None:
|
||||
stream = io.BytesIO()
|
||||
with pytest.raises(KlamathError, match="Record size is too big"):
|
||||
write_record_header(stream, 0x10000, 0x1234)
|
||||
|
||||
def test_read_record_header_errors() -> None:
|
||||
# Too small
|
||||
stream = io.BytesIO(struct.pack('>HH', 2, 0x1234))
|
||||
with pytest.raises(KlamathError, match="Record size is too small"):
|
||||
read_record_header(stream)
|
||||
|
||||
# Odd size
|
||||
stream = io.BytesIO(struct.pack('>HH', 5, 0x1234))
|
||||
with pytest.raises(KlamathError, match="Record size is odd"):
|
||||
read_record_header(stream)
|
||||
|
||||
def test_expect_record() -> None:
|
||||
stream = io.BytesIO()
|
||||
write_record_header(stream, 4, 0x1111)
|
||||
stream.seek(0)
|
||||
|
||||
# Correct tag
|
||||
size = expect_record(stream, 0x1111)
|
||||
assert size == 4
|
||||
|
||||
# Incorrect tag
|
||||
stream.seek(0)
|
||||
with pytest.raises(KlamathError, match="Unexpected record"):
|
||||
expect_record(stream, 0x2222)
|
||||
|
||||
def test_bitarray_record() -> None:
|
||||
class TestBit(BitArrayRecord):
|
||||
tag = 0x9999
|
||||
|
||||
stream = io.BytesIO()
|
||||
TestBit.write(stream, 0x8000)
|
||||
stream.seek(0)
|
||||
|
||||
val = TestBit.read(stream)
|
||||
assert val == 0x8000
|
||||
|
||||
def test_int2_record() -> None:
|
||||
class TestInt2(Int2Record):
|
||||
tag = 0x8888
|
||||
|
||||
stream = io.BytesIO()
|
||||
TestInt2.write(stream, [1, -2, 3])
|
||||
stream.seek(0)
|
||||
|
||||
val = TestInt2.read(stream)
|
||||
assert list(val) == [1, -2, 3]
|
||||
|
||||
def test_ascii_record() -> None:
|
||||
class TestASCII(ASCIIRecord):
|
||||
tag = 0x7777
|
||||
|
||||
stream = io.BytesIO()
|
||||
TestASCII.write(stream, b"HELLO")
|
||||
stream.seek(0)
|
||||
|
||||
val = TestASCII.read(stream)
|
||||
assert val == b"HELLO"
|
||||
|
||||
def test_datetime_record() -> None:
|
||||
class TestDT(DateTimeRecord):
|
||||
tag = 0x6666
|
||||
|
||||
now = datetime(2023, 10, 27, 12, 30, 45)
|
||||
stream = io.BytesIO()
|
||||
TestDT.write(stream, [now, now])
|
||||
stream.seek(0)
|
||||
|
||||
vals = TestDT.read(stream)
|
||||
assert vals == [now, now]
|
||||
|
||||
def test_nodata_record() -> None:
|
||||
class TestNoData(NoDataRecord):
|
||||
tag = 0x5555
|
||||
|
||||
stream = io.BytesIO()
|
||||
TestNoData.write(stream, None)
|
||||
stream.seek(0)
|
||||
|
||||
# Verify header: 4 bytes total (size=4, tag=0x5555), data_size=0
|
||||
header = stream.read(4)
|
||||
assert header == struct.pack('>HH', 4, 0x5555)
|
||||
|
||||
stream.seek(0)
|
||||
assert TestNoData.read(stream) is None
|
||||
|
||||
def test_record_skip_past() -> None:
|
||||
stream = io.BytesIO()
|
||||
HEADER.write(stream, 600)
|
||||
ENDLIB.write(stream, None)
|
||||
|
||||
stream.seek(0)
|
||||
# Skip past HEADER
|
||||
found = HEADER.skip_past(stream)
|
||||
assert found is True
|
||||
assert stream.tell() == 6 # 4 header + 2 data
|
||||
|
||||
# Try to skip past something that doesn't exist before ENDLIB
|
||||
class NONEXISTENT(NoDataRecord):
|
||||
tag = 0xFFFF
|
||||
|
||||
stream.seek(0)
|
||||
found = NONEXISTENT.skip_past(stream)
|
||||
assert found is False
|
||||
# Should be at the end of ENDLIB record header/tag read
|
||||
assert stream.tell() == 10 # 6 (HEADER) + 4 (ENDLIB)
|
||||
110
klamath/test_records.py
Normal file
110
klamath/test_records.py
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
import io
|
||||
import pytest
|
||||
import numpy
|
||||
from datetime import datetime
|
||||
from klamath.basic import KlamathError
|
||||
from klamath import records
|
||||
|
||||
def test_record_tags() -> None:
|
||||
assert records.HEADER.tag == 0x0002
|
||||
assert records.BGNLIB.tag == 0x0102
|
||||
assert records.LIBNAME.tag == 0x0206
|
||||
assert records.UNITS.tag == 0x0305
|
||||
assert records.ENDLIB.tag == 0x0400
|
||||
assert records.BGNSTR.tag == 0x0502
|
||||
assert records.STRNAME.tag == 0x0606
|
||||
assert records.ENDSTR.tag == 0x0700
|
||||
assert records.BOUNDARY.tag == 0x0800
|
||||
assert records.PATH.tag == 0x0900
|
||||
assert records.SREF.tag == 0x0a00
|
||||
assert records.AREF.tag == 0x0b00
|
||||
assert records.TEXT.tag == 0x0c00
|
||||
assert records.LAYER.tag == 0x0d02
|
||||
assert records.DATATYPE.tag == 0x0e02
|
||||
assert records.WIDTH.tag == 0x0f03
|
||||
assert records.XY.tag == 0x1003
|
||||
assert records.ENDEL.tag == 0x1100
|
||||
assert records.SNAME.tag == 0x1206
|
||||
assert records.COLROW.tag == 0x1302
|
||||
assert records.NODE.tag == 0x1500
|
||||
assert records.TEXTTYPE.tag == 0x1602
|
||||
assert records.PRESENTATION.tag == 0x1701
|
||||
assert records.STRING.tag == 0x1906
|
||||
assert records.STRANS.tag == 0x1a01
|
||||
assert records.MAG.tag == 0x1b05
|
||||
assert records.ANGLE.tag == 0x1c05
|
||||
assert records.REFLIBS.tag == 0x1f06
|
||||
assert records.FONTS.tag == 0x2006
|
||||
assert records.PATHTYPE.tag == 0x2102
|
||||
assert records.GENERATIONS.tag == 0x2202
|
||||
assert records.ATTRTABLE.tag == 0x2306
|
||||
assert records.ELFLAGS.tag == 0x2601
|
||||
assert records.NODETYPE.tag == 0x2a02
|
||||
assert records.PROPATTR.tag == 0x2b02
|
||||
assert records.PROPVALUE.tag == 0x2c06
|
||||
assert records.BOX.tag == 0x2d00
|
||||
assert records.BOXTYPE.tag == 0x2e02
|
||||
assert records.PLEX.tag == 0x2f03
|
||||
assert records.BGNEXTN.tag == 0x3003
|
||||
assert records.ENDEXTN.tag == 0x3103
|
||||
assert records.TAPENUM.tag == 0x3202
|
||||
assert records.TAPECODE.tag == 0x3302
|
||||
assert records.FORMAT.tag == 0x3602
|
||||
assert records.MASK.tag == 0x3706
|
||||
assert records.ENDMASKS.tag == 0x3800
|
||||
assert records.LIBDIRSIZE.tag == 0x3902
|
||||
assert records.SRFNAME.tag == 0x3a06
|
||||
assert records.LIBSECUR.tag == 0x3b02
|
||||
|
||||
def test_header_validation() -> None:
|
||||
# Correct size
|
||||
records.HEADER.check_size(2)
|
||||
|
||||
# Incorrect size
|
||||
with pytest.raises(KlamathError, match="Expected size 2, got 4"):
|
||||
records.HEADER.check_size(4)
|
||||
|
||||
def test_bgnlib_validation() -> None:
|
||||
now = datetime(2023, 10, 27, 12, 30, 45)
|
||||
# Correct size (2 datetimes = 24 bytes)
|
||||
records.BGNLIB.check_size(24)
|
||||
|
||||
# Incorrect size
|
||||
with pytest.raises(KlamathError, match="Expected size 24, got 12"):
|
||||
records.BGNLIB.check_size(12)
|
||||
|
||||
def test_reflibs_fonts_validation() -> None:
|
||||
# REFLIBS must be multiple of 44
|
||||
records.REFLIBS.check_size(44)
|
||||
records.REFLIBS.check_size(88)
|
||||
records.REFLIBS.check_size(0)
|
||||
|
||||
with pytest.raises(KlamathError, match="Expected size to be multiple of 44"):
|
||||
records.REFLIBS.check_size(10)
|
||||
|
||||
def test_generations_format_validation() -> None:
|
||||
# GENERATIONS expects exactly one integer
|
||||
records.GENERATIONS.check_data(3)
|
||||
records.GENERATIONS.check_data([1])
|
||||
|
||||
with pytest.raises(KlamathError, match="Expected exactly one integer"):
|
||||
records.GENERATIONS.check_data([1, 2])
|
||||
|
||||
def test_attrtable_validation() -> None:
|
||||
# ATTRTABLE size <= 44
|
||||
records.ATTRTABLE.check_size(44)
|
||||
records.ATTRTABLE.check_size(10)
|
||||
|
||||
with pytest.raises(KlamathError, match="Expected size <= 44"):
|
||||
records.ATTRTABLE.check_size(45)
|
||||
|
||||
def test_nodata_records() -> None:
|
||||
stream = io.BytesIO()
|
||||
records.ENDLIB.write(stream, None)
|
||||
stream.seek(0)
|
||||
assert records.ENDLIB.read(stream) is None
|
||||
|
||||
stream = io.BytesIO()
|
||||
records.BOUNDARY.write(stream, None)
|
||||
stream.seek(0)
|
||||
assert records.BOUNDARY.read(stream) is None
|
||||
|
|
@ -1,7 +1,3 @@
|
|||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "klamath"
|
||||
description = "GDSII format reader/writer"
|
||||
|
|
@ -44,6 +40,7 @@ classifiers = [
|
|||
"Intended Audience :: Science/Research",
|
||||
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
||||
"Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)",
|
||||
"Topic :: File Formats",
|
||||
]
|
||||
requires-python = ">=3.11"
|
||||
include = [
|
||||
|
|
@ -54,6 +51,11 @@ dependencies = [
|
|||
"numpy>=1.26",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
|
||||
[tool.hatch.version]
|
||||
path = "klamath/__init__.py"
|
||||
|
||||
|
|
@ -78,7 +80,6 @@ lint.ignore = [
|
|||
"ANN002", # *args
|
||||
"ANN003", # **kwargs
|
||||
"ANN401", # Any
|
||||
"ANN101", # self: Self
|
||||
"SIM108", # single-line if / else assignment
|
||||
"RET504", # x=y+z; return x
|
||||
"PIE790", # unnecessary pass
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue