Skip to content

Commit d0ae059

Browse files
committed
ci: add unit tests
1 parent e867086 commit d0ae059

File tree

9 files changed

+1274
-111
lines changed

9 files changed

+1274
-111
lines changed

.github/workflows/ci.yml

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
name: CI
2+
on:
3+
push:
4+
pull_request:
5+
# release:
6+
# types: [released]
7+
workflow_dispatch:
8+
9+
env:
10+
DEFAULT_PYTHON: "3.12"
11+
12+
jobs:
13+
build:
14+
name: 🔨 Build distribution
15+
runs-on: ubuntu-latest
16+
steps:
17+
- uses: actions/checkout@v4
18+
19+
- name: 🏗 Set up Python ${{ env.DEFAULT_PYTHON }}
20+
uses: actions/setup-python@v5
21+
with:
22+
python-version: "${{ env.DEFAULT_PYTHON }}"
23+
24+
- name: 🔨 Build distribution
25+
uses: OctoPrint/actions/build-dist@main
26+
with:
27+
artifact: dist
28+
29+
pre-commit:
30+
name: 🧹 Pre-commit
31+
runs-on: ubuntu-latest
32+
steps:
33+
- uses: actions/checkout@v4
34+
35+
- name: 🏗 Set up Python ${{ env.DEFAULT_PYTHON }}
36+
uses: actions/setup-python@v4
37+
with:
38+
python-version: "${{ env.DEFAULT_PYTHON }}"
39+
40+
- name: 🏗 Set up dev dependencies
41+
run: |
42+
pip install -e .[develop]
43+
44+
- name: 🚀 Run pre-commit
45+
run: |
46+
pre-commit run --all-files --show-diff-on-failure
47+
48+
test-unit:
49+
name: 🧪 Unit tests
50+
strategy:
51+
fail-fast: false
52+
matrix:
53+
python: ["3.9", "3.10", "3.11", "3.12", "3.13"]
54+
runs-on: ubuntu-latest
55+
steps:
56+
- uses: actions/checkout@v4
57+
58+
- name: 🏗 Set up Python ${{ matrix.python }}
59+
uses: actions/setup-python@v4
60+
with:
61+
python-version: ${{ matrix.python }}
62+
63+
- name: 🏗 Set up test dependencies
64+
run: |
65+
pip install -e .[develop]
66+
67+
- name: 🚀 Run test suite
68+
run: |
69+
pytest | tee report.txt
70+
71+
# generate summary
72+
python=$(python -c 'import sys; print(".".join(map(str, sys.version_info[:3])))')
73+
today=$(date +'%Y-%m-%d')
74+
now=$(date +'%H:%M')
75+
summary=$(tail -n1 report.txt | sed 's/^=*\s//g' | sed 's/\s=*$//g')
76+
77+
cat << EOF >> $GITHUB_STEP_SUMMARY
78+
### Test Report
79+
80+
*generated on $today at $now under Python $python*
81+
82+
<details>
83+
<summary>$summary</summary>
84+
85+
\`\`\`
86+
$(cat report.txt)
87+
\`\`\`
88+
89+
</details>
90+
EOF
91+
92+
name: 🧪 Installation test
93+
strategy:
94+
fail-fast: false
95+
matrix:
96+
python: ["3.9", "3.10", "3.11", "3.12", "3.13"]
97+
runs-on: ubuntu-latest
98+
steps:
99+
- uses: actions/checkout@v4
100+
101+
- name: 🏗 Set up Python ${{ matrix.python }}
102+
uses: actions/setup-python@v4
103+
with:
104+
python-version: ${{ matrix.python }}
105+
106+
- name: 🚀 Run test install
107+
run: |
108+
pip install -e .
109+
110+
publish-on-testpypi:
111+
name: 📦 Publish on TestPyPI
112+
if: github.event_name == 'release'
113+
needs:
114+
- build
115+
- pre-commit
116+
- test-install
117+
- test-unit
118+
runs-on: ubuntu-latest
119+
120+
environment:
121+
name: testpypi
122+
url: https://test.pypi.org/p/gcode-thumbnail-tool
123+
124+
permissions:
125+
id-token: write
126+
127+
steps:
128+
- name: ⬇ Download build result
129+
uses: actions/download-artifact@v4
130+
with:
131+
name: dist
132+
path: dist
133+
134+
- name: 🧹 Remove some stuff that won't make it through twine check
135+
run: |
136+
rm dist/*.source.tar.gz
137+
rm dist/sha512sums.txt
138+
139+
- name: 📦 Publish to index
140+
uses: pypa/gh-action-pypi-publish@release/v1
141+
with:
142+
repository-url: https://test.pypi.org/legacy/
143+
144+
publish-on-pypi:
145+
name: 📦 Publish tagged releases to PyPI
146+
if: github.event_name == 'release'
147+
needs: publish-on-testpypi
148+
runs-on: ubuntu-latest
149+
150+
environment:
151+
name: pypi
152+
url: https://pypi.org/p/gcode-thumbnail-tool
153+
154+
permissions:
155+
id-token: write
156+
157+
steps:
158+
- name: ⬇ Download build result
159+
uses: actions/download-artifact@v4
160+
with:
161+
name: dist
162+
path: dist
163+
164+
- name: 🧹 Remove some stuff that won't make it through twine check
165+
run: |
166+
rm dist/*.source.tar.gz
167+
rm dist/sha512sums.txt
168+
169+
- name: 📦 Publish to index
170+
uses: pypa/gh-action-pypi-publish@release/v1

gcode_thumbnail_tool.py

Lines changed: 47 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ class ExtractedBytes:
129129
images: dict[str, bytes]
130130

131131

132-
def _potential_lines(path: str) -> str:
132+
def _potential_lines_from_gcode_file(path: str) -> str:
133133
if not os.path.exists(path):
134134
return ""
135135

@@ -151,20 +151,40 @@ def _potential_lines(path: str) -> str:
151151
return "".join(content_lines).replace("\r\n", "\n").replace(";\n;\n", ";\n\n;\n")
152152

153153

154-
def extract_thumbnails_from_gcode(gcode_path: str) -> Optional[ExtractedImages]:
154+
def _to_thumbnail_bytes(extracted: ExtractedImages, format="PNG") -> ExtractedBytes:
155+
data = {}
156+
for image in extracted.images:
157+
sizehint = _image_to_sizehint(image)
158+
if sizehint in data:
159+
continue
160+
data[sizehint] = _image_to_bytes(image, format=format)
161+
return ExtractedBytes(extractor=extracted.extractor, images=data)
162+
163+
164+
def extract_thumbnails_from_gcode_file(gcode_path: str) -> Optional[ExtractedImages]:
155165
logger = logging.getLogger(__name__)
156166

167+
if not os.path.exists(gcode_path):
168+
logger.warning(f"Gcode file {gcode_path} doesn't exist")
169+
return None
170+
157171
logger.info(f"Extracting thumbnails from {gcode_path}...")
158172

159-
potential_lines = _potential_lines(gcode_path)
173+
potential_lines = _potential_lines_from_gcode_file(gcode_path)
160174
logger.debug(
161175
f"Searching for matches in:\n{_prefix_lines(potential_lines, prefix=' | ')}"
162176
)
163177

178+
return extract_thumbnails_from_gcode(potential_lines)
179+
180+
181+
def extract_thumbnails_from_gcode(gcode: str) -> Optional[ExtractedImages]:
182+
logger = logging.getLogger(__name__)
183+
164184
extractors = [
165185
("generic", (REGEX_GENERIC, _extract_generic_base64_thumbnails)),
166186
("snapmaker", (REGEX_SNAPMAKER, _extract_generic_base64_thumbnails)),
167-
("mks", (REGEX_MKS, _extract_mks_thumbnails)),
187+
# ("mks", (REGEX_MKS, _extract_mks_thumbnails)),
168188
("weedo", (REGEX_WEEDO, _extract_weedo_thumbnails)),
169189
("qidi", (REGEX_QIDI, _extract_qidi_thumbnails)),
170190
("flashprint", _extract_flashprint_thumbnails),
@@ -176,45 +196,39 @@ def extract_thumbnails_from_gcode(gcode_path: str) -> Optional[ExtractedImages]:
176196
# regex based extractor
177197
regex, extractor = tooling
178198

179-
matches = list(regex.finditer(potential_lines))
199+
matches = list(regex.finditer(gcode))
180200
if matches:
181201
logger.debug(f"Detected {name} thumbnails, extracting...")
182202
return ExtractedImages(extractor=name, images=extractor(matches))
183203

184204
elif callable(tooling):
185205
# custom extractor function
186-
thumbnails = tooling(gcode_path, potential_lines)
206+
thumbnails = tooling(gcode)
187207
if thumbnails:
188208
return ExtractedImages(extractor=name, images=thumbnails)
189209

190-
# none of the regex based extractors matched, could this be flashprint?
191-
with open(gcode_path, "rb") as f:
192-
f.seek(58)
193-
buffer = f.read(14454)
194-
if buffer[0] == 0x42 and buffer[1] == 0x4D: # BMP magic numbers
195-
logger.debug("Detected flashprint thumbnails, extracting...")
196-
return ExtractedImages(
197-
extractor="flashprint", images=_extract_flashprint_thumbnails(buffer)
198-
)
199-
200210
# if we reach this point, we could not find any thumbnails
201211
return None
202212

203213

204-
def extract_thumbnail_bytes_from_gcode(
214+
def extract_thumbnail_bytes_from_gcode_file(
205215
gcode_path: str, format="PNG"
206216
) -> Optional[ExtractedBytes]:
207-
result = extract_thumbnails_from_gcode(gcode_path)
217+
result = extract_thumbnails_from_gcode_file(gcode_path)
208218
if not result:
209219
return None
210220

211-
data = {}
212-
for image in result.images:
213-
sizehint = _image_to_sizehint(image)
214-
if sizehint in data:
215-
continue
216-
data[sizehint] = _image_to_bytes(image, format=format)
217-
return ExtractedBytes(extractor=result.extractor, images=data)
221+
return _to_thumbnail_bytes(result, format=format)
222+
223+
224+
def extract_thumbnail_bytes_from_gcode(
225+
gcode: str, format="PNG"
226+
) -> Optional[ExtractedBytes]:
227+
result = extract_thumbnails_from_gcode(gcode)
228+
if not result:
229+
return None
230+
231+
return _to_thumbnail_bytes(result, format=format)
218232

219233

220234
# ~~ extractors
@@ -245,24 +259,7 @@ def _extract_generic_base64_thumbnails(matches: list[re.Match]) -> list[PILImage
245259
return result
246260

247261

248-
def _extract_generic_hex_thumbnails(matches: list[re.Match]) -> list[PILImage]:
249-
"""
250-
Extracts thumbnails from hex encoded data
251-
252-
Will remove any comment prefixes from lines.
253-
254-
Expected match groups:
255-
* ``data``: hex encoded data
256-
"""
257-
result = []
258-
for match in matches:
259-
data = _remove_whitespace(_remove_comment_prefix(match.group("data")))
260-
image = _image_from_hex(data)
261-
result.append(image)
262-
return result
263-
264-
265-
def _extract_mks_thumbnails(matches: list[re.Match]) -> list[PILImage]:
262+
def _extract_old_mks_thumbnails(matches: list[re.Match]) -> list[PILImage]:
266263
"""Extracts a thumbnail from hex binary data used by MKS printers"""
267264

268265
OPTIONS = {";;gimage": (200, 200), ";simage": (100, 100)}
@@ -392,11 +389,14 @@ def val2rgb(val: int) -> tuple[int, int, int]:
392389
return result
393390

394391

395-
def _extract_flashprint_thumbnails(path: str, _lines: str) -> list[PILImage]:
396-
with open(path, "rb") as f:
392+
def _extract_flashprint_thumbnails(gcode: str) -> list[PILImage]:
393+
with io.BytesIO(gcode.encode()) as f:
397394
f.seek(58)
398395
buffer = f.read(14454)
399396

397+
if not len(buffer) == 14454:
398+
return {}
399+
400400
if not (buffer[0] == 0x42 and buffer[1] == 0x4D): # BMP magic numbers
401401
return {}
402402

@@ -534,7 +534,7 @@ def main():
534534
print(f"{args.path} doesn't exist, exiting!", file=sys.stderr)
535535
sys.exit(1)
536536

537-
result = extract_thumbnail_bytes_from_gcode(args.path, format="PNG")
537+
result = extract_thumbnail_bytes_from_gcode_file(args.path, format="PNG")
538538
if result:
539539
output_folder = args.output
540540
if not output_folder:
@@ -559,7 +559,7 @@ def main():
559559
print(f"{args.path} doesn't exist, exiting!", file=sys.stderr)
560560
sys.exit(1)
561561

562-
result = extract_thumbnails_from_gcode(args.path)
562+
result = extract_thumbnails_from_gcode_file(args.path)
563563
if result:
564564
print(
565565
f'Found {len(result.images)} thumbnails in {args.path}, in format "{result.extractor}":'

pyproject.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,11 @@ maintainers = [
2121

2222
[project.scripts]
2323
gcode-thumbnail-tool = "gcode_thumbnail_tool:main"
24+
25+
[project.optional-dependencies]
26+
develop = [
27+
# Testing dependencies
28+
"pytest-doctest-custom>=1.0.0,<2",
29+
"pytest>=8.3.4,<9",
30+
"pre-commit",
31+
]

tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Tests for gcode-thumbnail-tool

0 commit comments

Comments
 (0)