Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
d573b1f
Created a JMPEntry wrapper class for JMPEntry and utilized it in load…
SomeJakeGuy Dec 31, 2025
d5dadbf
Added an equality method for JMPFieldHeader
SomeJakeGuy Dec 31, 2025
5143666
Make JMPEntry available for imports as well.
SomeJakeGuy Dec 31, 2025
307f8e7
Re-order the functions so the jmp fields add and map functions are ne…
SomeJakeGuy Dec 31, 2025
bb377e6
Publicized find jmp field function and merged it into one function ra…
SomeJakeGuy Dec 31, 2025
19dccc4
Introduce new error in add jmp field if it already exists.
SomeJakeGuy Dec 31, 2025
530661f
Created a delete jmp header function that will automatically remove t…
SomeJakeGuy Dec 31, 2025
ab1a39c
Renamed find_field to find_jmp_header
SomeJakeGuy Dec 31, 2025
a4ab57d
Removing various get/check/update functions in JMP class in favor of …
SomeJakeGuy Dec 31, 2025
a4adcb5
Updated docstring for delete_jmp_header
SomeJakeGuy Dec 31, 2025
d5942cd
Added a docstring for find_jmp_header
SomeJakeGuy Dec 31, 2025
b2ca858
Privatized data_entries and created a getter to get the property values.
SomeJakeGuy Dec 31, 2025
76924a5
Created a delete_jmp_entry function based on a direct entry / index n…
SomeJakeGuy Dec 31, 2025
aeadc9b
Added an add_jmp_entry function that validates the JMPEntry as it's t…
SomeJakeGuy Dec 31, 2025
cdad59c
Created a JMP clear data entry function.
SomeJakeGuy Dec 31, 2025
11cbe4a
Update hash function to use id instead of field_hash
SomeJakeGuy Dec 31, 2025
8779c14
Corrected data_entries local var in _load_entries to not have an unde…
SomeJakeGuy Dec 31, 2025
f343fe2
Created some validate functions for field headers.
SomeJakeGuy Dec 31, 2025
93532c2
Publicize validate all jmp entries. Changed some validations regardin…
SomeJakeGuy Dec 31, 2025
8013232
Removed update field header list, as JMPFieldHeader order is actually…
SomeJakeGuy Dec 31, 2025
394e1d3
Updated some of the JMP documentation.
SomeJakeGuy Dec 31, 2025
2aa6dfe
Bumps this version up a major, as several things were refactored in J…
SomeJakeGuy Dec 31, 2025
d988f7c
Updated some doc strings and provided a default variable to avoid typ…
SomeJakeGuy Dec 31, 2025
9713e04
Forced string output the KeyError details in get/set item
SomeJakeGuy Dec 31, 2025
81a6b92
Gets a prm entry from the list based on the provided field_name or fi…
SomeJakeGuy Dec 31, 2025
e48565a
Adjust JMPEntry to utilize one find entry function and combine errors…
SomeJakeGuy Dec 31, 2025
8ce3edb
Create the requirement.txt file specifically for use in GitHub action…
SomeJakeGuy Dec 31, 2025
60153d6
Created some initial JMP error tests.
SomeJakeGuy Dec 31, 2025
377ef9b
Initial draft of including pytest in Publish Release yml.
SomeJakeGuy Dec 31, 2025
75664fb
Finalized draft of testing JMP files.
SomeJakeGuy Jan 2, 2026
ce94157
Unit Test GH Action to check unit-tests work prior to publishes.
SomeJakeGuy Jan 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .github/workflows/python-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,29 @@ permissions:
contents: read

jobs:
pytest-code:
runs: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install dependencies
run: python -m pip install --upgrade pip setuptools wheel -r requirements.txt

- name: Test with pytest
run: |
coverage run -m pytest -v -s

- name: Generate Coverage Report
run: |
coverage report -m

release-build:
runs-on: ubuntu-latest
needs: [pytest-code]

steps:
- uses: actions/checkout@v4
Expand Down
25 changes: 25 additions & 0 deletions .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Unit Test GCBrickWork

on:
workflow_dispatch:

jobs:
pytest-code:
runs: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install dependencies
run: python -m pip install --upgrade pip setuptools wheel -r requirements.txt

- name: Test with pytest
run: |
coverage run -m pytest -v -s

- name: Generate Coverage Report
run: |
coverage report -m
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ The structure of these files break down in the following way:

### Jump / JMP
These types of files typically table-like structures that are loaded into RAM during run-time.
These files are similar to modern day data-tables.
* JMP Files contain a giant header block and data entry block.
* The header block contains the definition of all field headers (columns) and field level data. Loads the first 16 bytes to determine (in order):
* How many data entries there are
Expand All @@ -50,6 +51,7 @@ These types of files typically table-like structures that are loaded into RAM du
* The next 2 bytes represent the starting byte for the field within a given data line in the JMP file.
* The second to last byte represents the shift bytes, which is required when reading certain field data.
* The last byte represents the data type, as defined as either Int, Str, or Floats.
* Order of the JMPFileHeaders does not matter in JMP files, as long as all fields used are defined.
* The data block contains the table row data one line at a time.
* Each row is represented by multiple columns of data, each of which should match to a JMP field header and its respective value type (Int, Str, Float, etc.)
* It should be noted that there will be extra bytes typically at the end of a jmp file, which are padded with "@".
Expand Down
341 changes: 210 additions & 131 deletions gcbrickwork/JMP.py

Large diffs are not rendered by default.

27 changes: 20 additions & 7 deletions gcbrickwork/PRM.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ def __str__(self):


class PRM:
""" PRM Files are parameterized files that have one or more parameters that can be changed/manipulated.
These files typically host values that would change frequently and are read by the program at run-time.
PRM Files start with 4 bytes as an unsigned int to tell how many parameters are defined.
The structure of the entries can be found in PRMFieldEntry. """
data_entries: list[PRMFieldEntry] = []


Expand All @@ -98,12 +102,9 @@ def __init__(self, input_entries: list[PRMFieldEntry]):

@classmethod
def load_prm(cls, prm_data: BytesIO):
""" Loads the various prm values from the file into a list of PRMFieldEntries
"""
PRM Files are parameterized files that have one or more parameters that can be changed/manipulated.
These files typically host values that would change frequently and are read by the program at run-time.
PRM Files start with 4 bytes as an unsigned int to tell how many parameters are defined.
The structure of the entries can be found in PRMFieldEntry
"""
entry_value: PRMValue | None = None
prm_entries: list[PRMFieldEntry] = []
current_offset: int = 0
num_of_entries: int = read_u32(prm_data, 0)
Expand Down Expand Up @@ -179,5 +180,17 @@ def create_new_prm(self) -> BytesIO:
return local_data


def get_entry(self, field_name: str) -> PRMFieldEntry:
return next(entry for entry in self.data_entries if entry.field_name == field_name)
def get_prm_entry(self, prm_field: str | int) -> PRMFieldEntry:
"""Gets a PRMFieldEntry based on a provided field name/hash."""
if isinstance(prm_field, str):
return next(prm_entry for prm_entry in self.data_entries if prm_entry.field_name == prm_field)
elif isinstance(prm_field, int):
return next(prm_entry for prm_entry in self.data_entries if prm_entry.field_hash == prm_field)
else:
raise ValueError(f"Cannot index PRMFieldEntry with value of type {type(prm_field)}")


def update_prm_entry(self, prm_field: str | int, prm_value: PRMValue):
"""Updates a PRMFieldEntry based on a provided field/value."""
prm_entry: PRMFieldEntry = self.get_prm_entry(prm_field)
prm_entry.field_value = prm_value
2 changes: 1 addition & 1 deletion gcbrickwork/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
from gcbrickwork.PRM import PRM, PRMType, PRMVector, PRMColor, PRMFieldEntry
from gcbrickwork.JMP import JMP, JMPType, JMPFieldHeader
from gcbrickwork.JMP import JMP, JMPType, JMPFieldHeader, JMPEntry
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pytest==9.0.2
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
setuptools.setup(
name="gcbrickwork",
packages=setuptools.find_packages(),
version="2.1.4",
version="3.0.0",
license="MIT",
author="Some Jake Guy",
author_email="somejakeguy@gmail.com",
Expand Down
Empty file added unit_tests/__init__.py
Empty file.
176 changes: 176 additions & 0 deletions unit_tests/test_jmp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import pytest, struct
from io import BytesIO
from sys import exception

from gcbrickwork import JMP
from gcbrickwork.Bytes_Helper import ByteHelperError
from gcbrickwork.JMP import JMPFileError


def _jmp_sixteen_header(field_count: int=0, entry_count: int=0, header_size: int=0, entry_size: int=0) -> BytesIO:
"""Writes a quick jmp where only the first 16 bytes are specified."""
# Calculate sizes
field_count = field_count
data_entry_count = entry_count
header_block_size = header_size
single_entry_size = entry_size

io_data = BytesIO()
io_data.write(struct.pack(">i", data_entry_count)) # Offset 0: data_entry_count (s32)
io_data.write(struct.pack(">i", field_count)) # Offset 4: field_count (s32)
io_data.write(struct.pack(">I", header_block_size)) # Offset 8: header_block_size (u32)
io_data.write(struct.pack(">I", single_entry_size)) # Offset 12: single_entry_size (u32)
return io_data

def _create_sample_jmp() -> BytesIO:
"""Creates a valid JMP file with 2 fields and 2 entries"""

# Define field headers
field1_hash = 0x12345678
field1_bitmask = 0xFFFFFFFF # Will be packed/unpacked as is
field1_start_byte = 0
field1_shift_byte = 0
field1_type = 0 # JMPType.Int

field2_hash = 0xABCDEF01
field2_bitmask = 0
field2_start_byte = 4
field2_shift_byte = 0
field2_type = 2 # JMPType.Flt

field3_hash = 0xCCCCAAAA
field3_bitmask = 0xFF # Will be masked as needed
field3_start_byte = 8
field3_shift_byte = 0
field3_type = 0 # JMPType.Int

field4_hash = 0xDDDDBBBB
field4_bitmask = 0x3F00 # Will be masked as needed
field4_start_byte = 8
field4_shift_byte = 8
field4_type = 0 # JMPType.Int


# Calculate sizes
field_count = 4
data_entry_count = 2
header_block_size = 16 + (field_count * 12)
single_entry_size = 8

jmp_data: BytesIO = _jmp_sixteen_header(field_count, data_entry_count, header_block_size, single_entry_size)

# Write field headers (24 bytes total, 12 bytes each)
jmp_data.write(struct.pack(">I", field1_hash))
jmp_data.write(struct.pack(">I", field1_bitmask))
jmp_data.write(struct.pack(">H", field1_start_byte))
jmp_data.write(struct.pack(">B", field1_shift_byte))
jmp_data.write(struct.pack(">B", field1_type))

jmp_data.write(struct.pack(">I", field2_hash))
jmp_data.write(struct.pack(">I", field2_bitmask))
jmp_data.write(struct.pack(">H", field2_start_byte))
jmp_data.write(struct.pack(">B", field2_shift_byte))
jmp_data.write(struct.pack(">B", field2_type))

jmp_data.write(struct.pack(">I", field3_hash))
jmp_data.write(struct.pack(">I", field3_bitmask))
jmp_data.write(struct.pack(">H", field3_start_byte))
jmp_data.write(struct.pack(">B", field3_shift_byte))
jmp_data.write(struct.pack(">B", field3_type))

jmp_data.write(struct.pack(">I", field4_hash))
jmp_data.write(struct.pack(">I", field4_bitmask))
jmp_data.write(struct.pack(">H", field4_start_byte))
jmp_data.write(struct.pack(">B", field4_shift_byte))
jmp_data.write(struct.pack(">B", field4_type))

# Write data entries (16 bytes total, 8 bytes each)
jmp_data.write(struct.pack(">I", 5))
jmp_data.write(struct.pack(">f", 100.0))
jmp_data.write(struct.pack(">I", 0 | ((5 << field3_shift_byte) & field3_bitmask) | ((42 << field4_shift_byte) & field4_bitmask)))

jmp_data.write(struct.pack(">I", 10))
jmp_data.write(struct.pack(">f", 200.0))
jmp_data.write(struct.pack(">I", 2660))

# Pad to 32-byte boundary with '@' characters
current_size = jmp_data.tell()
padding_needed = (32 - (current_size % 32)) % 32
if padding_needed > 0:
jmp_data.write(b'@' * padding_needed)

return jmp_data

def test_none_jmp_data():
"""Tests JMP type creation when None type is provided"""
with pytest.raises(AttributeError):
JMP.load_jmp(None)

def test_empty_jmp_data():
"""Tests JMP type creation when empty BytesIO is provided"""
with pytest.raises(ByteHelperError):
JMP.load_jmp(BytesIO())

def test_jmp_first_sixteen_bytes():
"""Tests JMP type creation when only the first 16 bytes are provided"""
with pytest.raises(JMPFileError):
JMP.load_jmp(_jmp_sixteen_header())

def test_full_jmp():
"""Tests the whole JMP file is read correctly"""
try:
JMP.load_jmp(_create_sample_jmp())
except exception as ex:
raise pytest.fail("Reading JMP Sample raised an exception: {0}".format(ex))

def test_jmp_save():
"""Ensures JMP file can be saved as expected."""
try:
temp_jmp: JMP = JMP.load_jmp(_create_sample_jmp())
temp_jmp.create_new_jmp()
except exception as ex:
raise pytest.fail("Saving JMP Sample raised an exception: {0}".format(ex))

def test_non_jmp_header_type_get():
"""Checks if an invalid JMP Header type is used to get a key"""
temp_jmp: JMP = JMP.load_jmp(_create_sample_jmp())
with pytest.raises(ValueError):
temp_jmp.data_entries[0][None] = []

def test_non_existent_jmp_header_type_get():
"""Checks for when a jmp header does not exist at all"""
temp_jmp: JMP = JMP.load_jmp(_create_sample_jmp())
with pytest.raises(KeyError):
temp_jmp.data_entries[0].__getitem__("Ch)eery")

def test_jmp_list_value_then_save():
"""Updates an entry to have a list valid, which is not valid and should error out."""
temp_jmp: JMP = JMP.load_jmp(_create_sample_jmp())
temp_jmp.data_entries[0][0x12345678] = []
with pytest.raises(struct.error):
temp_jmp.create_new_jmp()

def test_jmp_read_is_correct():
temp_jmp: JMP = JMP.load_jmp(_create_sample_jmp())
assert (temp_jmp.data_entries[0][0x12345678] == 5)
assert (temp_jmp.data_entries[0][0xABCDEF01] == 100.000000)
assert (temp_jmp.data_entries[0][0xCCCCAAAA] == 5)
assert (temp_jmp.data_entries[0][0xDDDDBBBB] == 42)

def test_jmp_read_save_then_reread():
"""Try to read, save, then re-read the data to check for data loss."""
try:
temp_jmp: JMP = JMP.load_jmp(_create_sample_jmp())
temp_data: BytesIO = temp_jmp.create_new_jmp()
JMP.load_jmp(temp_data)
except exception as ex:
raise pytest.fail("Reading, saving, then re-reading the JMP Sample raised an exception: {0}".format(ex))

def test_jmp_read_is_correct_after_reread():
temp_jmp: JMP = JMP.load_jmp(_create_sample_jmp())
temp_data: BytesIO = temp_jmp.create_new_jmp()
temp_jmp = JMP.load_jmp(temp_data)
assert (temp_jmp.data_entries[0][0x12345678] == 5)
assert (temp_jmp.data_entries[0][0xABCDEF01] == 100.000000)
assert (temp_jmp.data_entries[0][0xCCCCAAAA] == 5)
assert (temp_jmp.data_entries[0][0xDDDDBBBB] == 42)