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
32 changes: 32 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: CI

on:
pull_request:
push:
branches: [main]

jobs:
ascii-check:
name: ascii-check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Fail on non-ASCII bytes in src/*.py
run: python3 tools/ci/check_ascii.py

ironpython:
name: ironpython
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Download IronPython 2.7.12
shell: pwsh
run: |
curl.exe -sL -o ipy.zip https://github.com/IronLanguages/ironpython2/releases/download/ipy-2.7.12/IronPython.2.7.12.zip
Expand-Archive ipy.zip -DestinationPath ipy
- name: Compile all src files with IronPython 2.7
shell: pwsh
run: .\ipy\net45\ipy.exe tools\ci\compile_all.py
- name: Import smoke test with stubbed scriptengine
shell: pwsh
run: .\ipy\net45\ipy.exe tools\ci\import_smoke.py
35 changes: 35 additions & 0 deletions tools/ci/check_ascii.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""Fail if any file in src/ contains a non-ASCII byte.

CODESYS ScriptEngine is IronPython 2.7, which enforces PEP 263: source files
must be pure ASCII unless an encoding is declared. A stray em-dash or smart
quote in a comment makes CODESYS refuse to load the file at all. Python 3
py_compile cannot catch this because Python 3 source defaults to UTF-8.

Runs under Python 3 (CI) or Python 2 - byte scanning only, no imports.
"""

import os
import sys

SRC = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "src")

failures = []
for name in sorted(os.listdir(SRC)):
if not name.endswith(".py"):
continue
path = os.path.join(SRC, name)
with open(path, "rb") as f:
data = f.read()
line = 1
for i in range(len(data)):
b = data[i] if isinstance(data[i], int) else ord(data[i])
if b == 0x0A:
line += 1
elif b > 0x7F:
failures.append("%s:%d: non-ASCII byte 0x%02X" % (name, line, b))

for failure in failures:
print("FAIL " + failure)
if not failures:
print("OK all src/*.py files are pure ASCII")
sys.exit(1 if failures else 0)
37 changes: 37 additions & 0 deletions tools/ci/compile_all.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# REMEMBER: this is python 2.7 - runs under IronPython, the engine CODESYS embeds.
"""Compile every file in src/ with the real IronPython 2.7 compiler.

This catches Python 2 syntax errors the way CODESYS ScriptEngine would,
which py_compile under Python 3 cannot.

Note: IronPython's compile() of an in-memory string does NOT enforce PEP 263
(verified empirically; only the file-based execute/import paths do). Encoding
violations are covered by check_ascii.py, and by import_smoke.py for the
library modules, so this script owns the syntax class only.
"""
from __future__ import print_function

import os
import sys

SRC = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "src")

failures = []
for name in sorted(os.listdir(SRC)):
if not name.endswith(".py"):
continue
path = os.path.join(SRC, name)
f = open(path, "rU")
try:
source = f.read()
finally:
f.close()

try:
compile(source, path, "exec")
print("OK " + name)
except SyntaxError as e:
failures.append(name)
print("FAIL %s: %s" % (name, e))

sys.exit(1 if failures else 0)
39 changes: 39 additions & 0 deletions tools/ci/import_smoke.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# REMEMBER: this is python 2.7 - runs under IronPython, the engine CODESYS embeds.
"""Import every library module in src/ with a stubbed scriptengine.

Compilation alone misses module-scope mistakes (bad names in dispatch tables,
broken imports between modules). Importing executes module-level code, which
is as close as CI can get to CODESYS loading the scripts.

The script_*.py entry scripts are excluded: they execute their main logic at
import time and require a live CODESYS project.
"""
from __future__ import print_function

import os
import sys

HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.join(HERE, "..", "..", "src"))
sys.path.insert(0, HERE) # provides the scriptengine stub

MODULES = [
"object_type",
"util",
"entrypoint",
"import_export",
"communication_import_export",
"import_from_files",
"project_template",
]

failures = []
for module in MODULES:
try:
__import__(module)
print("OK import " + module)
except Exception as e:
failures.append(module)
print("FAIL import %s: %r" % (module, e))

sys.exit(1 if failures else 0)
25 changes: 25 additions & 0 deletions tools/ci/scriptengine.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# REMEMBER: this is python 2.7
"""Stub of the CODESYS-injected scriptengine module, for CI import tests only.

The real module exists solely inside the CODESYS ScriptEngine host. The src
modules only touch it inside function bodies, so an attribute sponge is enough
to let them import. If a future change accesses scriptengine at module scope
in a way this stub cannot satisfy, the import smoke test fails - which is
exactly the kind of regression it exists to catch.
"""


class _Anything(object):
def __getattr__(self, name):
return _Anything()

def __call__(self, *args, **kwargs):
return _Anything()


projects = _Anything()
system = _Anything()
online = _Anything()
ImplementationLanguages = _Anything()
PromptChoice = _Anything()
PromptResult = _Anything()
Loading