diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..5994bb5 --- /dev/null +++ b/.github/workflows/ci.yml @@ -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 diff --git a/tools/ci/check_ascii.py b/tools/ci/check_ascii.py new file mode 100644 index 0000000..c6b5286 --- /dev/null +++ b/tools/ci/check_ascii.py @@ -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) diff --git a/tools/ci/compile_all.py b/tools/ci/compile_all.py new file mode 100644 index 0000000..51b9561 --- /dev/null +++ b/tools/ci/compile_all.py @@ -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) diff --git a/tools/ci/import_smoke.py b/tools/ci/import_smoke.py new file mode 100644 index 0000000..d7eb9c1 --- /dev/null +++ b/tools/ci/import_smoke.py @@ -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) diff --git a/tools/ci/scriptengine.py b/tools/ci/scriptengine.py new file mode 100644 index 0000000..d828958 --- /dev/null +++ b/tools/ci/scriptengine.py @@ -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()