Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
40 changes: 40 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: CI

on:
pull_request:
push:
branches: [main]

jobs:
test:
name: pytest
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

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

# erbsland-maze depends on pycairo, which has no Linux wheel and builds
# against the system cairo headers.
- name: Install system libraries
run: |
sudo apt-get update
sudo apt-get install -y libcairo2-dev pkg-config

# erbsland-maze (the 'maze' extra) depends on pycairo, which has no Linux
# wheel and builds against the system cairo headers.
- name: Install system libraries
run: |
sudo apt-get update
sudo apt-get install -y libcairo2-dev pkg-config

- name: Install package + dev deps
run: |
python -m pip install --upgrade pip setuptools
python -m pip install ".[dev,maze]"
python -m pip list

- name: Run tests
run: python -m pytest -q
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ Getting started
This library is available from PyPi as `rollingmill`.

$ pip install rollingmill
$ pip install "rollingmill[maze]" # adds maze generator (needs cairo headers on Linux)
$ rollingmill # launch TUI on real hardware
$ rollingmill --file sample-nc/example-doc-part.gcode # stream an NC file

Expand Down
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ classifiers = [
dependencies = [
"pyusb>=1.2",
"pyhershey>=0.1",
"erbsland-maze>=1.4.8",
"setuptools", # pyhershey imports pkg_resources; drop when upstream moves to importlib.metadata
]

Expand All @@ -44,6 +43,9 @@ dev = [
"ruff>=0.6",
"mypy>=1.10",
]
maze = [
"erbsland-maze>=1.4.8", # imports pycairo, which builds against system cairo on Linux
]

[project.scripts]
rollingmill = "rollingmill.tui:main"
Expand Down
29 changes: 17 additions & 12 deletions rollingmill/maze.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,23 @@
from pathlib import Path
from typing import Iterable

from erbsland_maze import (
Direction,
Generator,
GeneratorSetup,
Room,
SvgLayout,
SvgSetup,
Wall,
)
from erbsland_maze.line import Line
from erbsland_maze.point import Point
from erbsland_maze.poly_line import PolyLine
try:
from erbsland_maze import (
Direction,
Generator,
GeneratorSetup,
Room,
SvgLayout,
SvgSetup,
Wall,
)
from erbsland_maze.line import Line
from erbsland_maze.point import Point
from erbsland_maze.poly_line import PolyLine
except ImportError as exc: # pragma: no cover - exercised only without the extra
raise ImportError(
"rollingmill.maze needs the 'maze' extra: pip install 'rollingmill[maze]'"
) from exc


ENDPOINT_MODES = ("none", "outline", "filled")
Expand Down
30 changes: 15 additions & 15 deletions tests/test_cutjob_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def bulk_write(self, data: bytes) -> int:
return len(data)


def _new_job(payload: bytes = b'G0X0\rG0Y0\rG0Z0\r') -> CutJob:
def _new_job(payload: bytes = b'G00 X0.0\rG00 Y0.0\rG00 Z0.0\r') -> CutJob:
m = MDX40A(_AckingMockUSB())
return CutJob.from_bytes(m, payload, 'test.nc')

Expand Down Expand Up @@ -156,17 +156,17 @@ def test_parse_nc_blocks_does_not_validate():
def test_check_nc_accepts_parse_nc_blocks_output_directly():
"""check_nc takes the raw bytes output of parse_nc_blocks; no decoding
needed at the call site."""
blocks = parse_nc_blocks(b"(hello)\nG01 X1.0\n(more)\n")
blocks = parse_nc_blocks(b"(hello)\nG00 X1.0\n(more)\n")
assert list(check_nc(blocks)) == []


def test_check_nc_clean_file_returns_no_warnings():
blocks = [b"(hello)\n", b"G01 X1.0\n", b"(more)\n"]
blocks = [b"(hello)\n", b"G00 X1.0\n", b"(more)\n"]
assert list(check_nc(blocks)) == []


def test_check_nc_flags_embedded_close_paren():
blocks = [b"G01 X1.0\n", b"(this (is) bad)\n", b"G01 Y2.0\n"]
blocks = [b"G00 X1.0\n", b"(this (is) bad)\n", b"G00 Y2.0\n"]
warnings = check_nc(blocks)
assert len(warnings) == 1
w = warnings[0]
Expand All @@ -182,19 +182,19 @@ def test_check_nc_flags_embedded_open_paren():


def test_check_nc_flags_unclosed_comment():
blocks = [b"G01 X1.0\n", b"G01 Y2.0\n", b"(forgot to close\n",
b"G01 Z3.0\n"]
blocks = [b"G00 X1.0\n", b"G00 Y2.0\n", b"(forgot to close\n",
b"G00 Z3.0\n"]
warnings = check_nc(blocks)
assert len(warnings) == 1
assert warnings[0].rule == RULE_PAREN_UNCLOSED
assert warnings[0].line == 3


def test_check_nc_ignores_parens_in_non_comment_lines():
# A code line that happens to contain '(' or ')' mid-stream is NOT
# subject to the rule -- only lines that *start* with '(' are
# treated as comments by the firmware.
assert list(check_nc([b"G01 X1.0 ( inline ) F100\n"])) == []
def test_check_nc_paren_rules_ignore_non_comment_lines():
# A code line that happens to contain '(' or ')' mid-stream is not a
# comment line, so rules 1 and 2 do not apply (rule 5 flags it instead).
warnings = check_nc([b"G00 X1.0 ( inline ) F100\n"])
assert [w.rule for w in warnings] == [5]


def test_check_nc_locates_column_of_first_embedded_paren():
Expand All @@ -211,7 +211,7 @@ def test_check_nc_accepts_empty_comment():

def test_check_nc_reports_all_warnings_not_just_first():
"""The lint should surface every issue so the user can fix in one pass."""
blocks = [b"(bad (one))\n", b"G01 X1.0\n", b"(unclosed\n"]
blocks = [b"(bad (one))\n", b"G00 X1.0\n", b"(unclosed\n"]
warnings = check_nc(blocks)
assert len(warnings) == 2
rules = sorted(w.rule for w in warnings)
Expand All @@ -231,7 +231,7 @@ def test_from_bytes_rejects_files_with_warnings():

def test_main_clean_file_returns_zero(tmp_path, capsys):
p = tmp_path / "clean.nc"
p.write_bytes(b"(ok)\nG01 X1.0\n")
p.write_bytes(b"(ok)\nG00 X1.0\n")
rc = check_main([str(p)])
out = capsys.readouterr().out
assert rc == 0
Expand All @@ -243,7 +243,7 @@ def test_main_reports_warnings_in_three_line_format(tmp_path, capsys):
three lines per warning (header, source, caret). capsys captures
non-TTY stdout, so ANSI codes are absent."""
p = tmp_path / "bad.nc"
p.write_bytes(b"G01 X1.0\n(bad (paren))\n")
p.write_bytes(b"G00 X1.0\n(bad (paren))\n")
rc = check_main([str(p)])
captured = capsys.readouterr()
assert rc == 1
Expand Down Expand Up @@ -276,7 +276,7 @@ def test_tui_main_bails_on_bad_file_before_usb(tmp_path, monkeypatch, capsys):
warnings and exit *before* MdxUSB.discover() is invoked — there's no
point handshaking the mill only to abort after reading the file."""
p = tmp_path / "bad.nc"
p.write_bytes(b"G01 X1.0\n(bad (paren))\n")
p.write_bytes(b"G00 X1.0\n(bad (paren))\n")

# Sentinel that fails the test if USB discovery is reached.
from rollingmill import usb as usb_mod
Expand Down
2 changes: 1 addition & 1 deletion tests/test_sleep_inhibit.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ def rel():
return counts


def _new_job(payload: bytes = b'G0X0\rG0Y0\rG0Z0\r') -> CutJob:
def _new_job(payload: bytes = b'G00 X0.0\rG00 Y0.0\rG00 Z0.0\r') -> CutJob:
return CutJob.from_bytes(MDX40A(_AckingMockUSB()), payload, 'test.nc')


Expand Down
Loading