diff --git a/.binary-allowlist b/.binary-allowlist
index b3f3f4ef..281172a6 100644
--- a/.binary-allowlist
+++ b/.binary-allowlist
@@ -18,9 +18,10 @@
# - the repo root, gates/, scripts/ and .github/ have no entry either, so a
# binary added there is blocked.
#
-# Every pattern here must also survive `git ls-files | git check-ignore
-# --stdin --no-index` printing nothing -- an allowlisted file that .gitignore
-# also matches fails silently on `git add` rather than erroring.
+# An allowlisted file that .gitignore ALSO matches fails silently on
+# `git add` rather than erroring. That is enforced, not remembered: the
+# ignore-consistency pre-commit hook (scripts/check_ignore_consistency.sh)
+# fails the commit if any tracked file is gitignored.
# --- Reports and presentations (155 .pdf, 3 .docx, 1 .doc, 1 .pptx) ---------
Programming/*.pdf
diff --git a/.github/workflows/runnability.yml b/.github/workflows/runnability.yml
new file mode 100644
index 00000000..604964de
--- /dev/null
+++ b/.github/workflows/runnability.yml
@@ -0,0 +1,40 @@
+name: runnability
+
+# Probes every course directory and fails if any RUNNER is broken.
+#
+# Deliberately NOT a diff against a committed baseline. A runner's status is a
+# property of the machine, not of the commit: MATLAB, Unity and the .NET SDK
+# are absent here and present locally, so `runnable` on a laptop and `blocked`
+# on ubuntu-latest is correct rather than drift. Diffing the two would make
+# this workflow red by construction, which is the one outcome the whole
+# exercise is meant to avoid.
+#
+# What IS a defect, and what this catches: a run.sh that crashes, hangs, or
+# reports an unknown kind. Those are `error`, and any `error` fails the job.
+on:
+ push:
+ branches: [main]
+ pull_request:
+ workflow_dispatch:
+
+jobs:
+ runnability:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 1
+
+ - name: Probe every course directory
+ run: ./run.sh status
+
+ - name: Fail on any broken runner
+ run: |
+ set -euo pipefail
+ if grep -qP '\terror\t' gates/runnability-ci.tsv; then
+ echo "::error::a course run.sh failed to probe"
+ grep -P '\terror\t' gates/runnability-ci.tsv
+ exit 1
+ fi
+ echo "all runners probed cleanly"
+ cut -f2 gates/runnability-ci.tsv | sort | uniq -c
diff --git a/.gitignore b/.gitignore
index 50dce689..3c449e44 100644
--- a/.gitignore
+++ b/.gitignore
@@ -482,3 +482,7 @@ Programming/ERSMS-project/nginx/ssl/nginx.crt
# Output of ./run.sh run
for the C++ courses.
build/
+
+# Generated fresh by the runner on each CI run. Committing a local copy
+# would label this machine capabilities as the runner is, which is a lie.
+gates/runnability-ci.tsv
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 8c508e6e..c56e2d52 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -56,6 +56,15 @@ repos:
language: script
pass_filenames: true
+ # A tracked file that .gitignore also matches fails silently on
+ # `git add`. 130 of those were fixed by hand; this keeps them fixed.
+ - id: ignore-consistency
+ name: no tracked file is gitignored
+ entry: scripts/check_ignore_consistency.sh
+ language: script
+ pass_filenames: false
+ always_run: true
+
# Thin shim into ~/utils, which owns the four markdown namespaces.
- id: md-naming
name: markdown naming convention
diff --git a/DOCS-runnability.md b/DOCS-runnability.md
index 06354bde..15ee9fad 100644
--- a/DOCS-runnability.md
+++ b/DOCS-runnability.md
@@ -2,53 +2,57 @@
Generated by `./run.sh status`. Do not edit by hand.
-Status is recorded PER ENVIRONMENT. A directory that is `runnable` locally and
+Status is recorded PER ENVIRONMENT, from `gates/runnability-local.tsv` and
+`gates/runnability-ci.tsv`. A directory that is `runnable` locally and
`blocked` in CI is the normal case, not drift: the runner has no MATLAB, no
-Unity and no .NET SDK. CI compares only its own column.
+Unity and no .NET SDK. Each environment rewrites only its own file, so a CI
+run cannot clobber the local column.
-| Course | local | Detail |
-|---|---|---|
-| `NotProgramming/DPZ` | runnable | builds the report with latexmk |
-| `NotProgramming/ECOGR` | runnable | builds the report with latexmk |
-| `NotProgramming/EDABA` | runnable | builds the report with latexmk |
-| `NotProgramming/EDABA-LAB` | runnable | builds the report with latexmk |
-| `NotProgramming/EDISE` | runnable | builds the report with latexmk |
-| `NotProgramming/EDISP` | no-code | submitted lab reports as PDFs only; no source was produced |
-| `NotProgramming/ELAC` | runnable | builds the report with latexmk |
-| `NotProgramming/EMISY` | runnable | builds the report with latexmk |
-| `NotProgramming/ENUME` | blocked | needs MATLAB or Octave (none of: matlab octave) |
-| `NotProgramming/EPHY2` | no-code | submitted lab reports as PDFs only; no source was produced |
-| `NotProgramming/EPOGAI` | no-code | a presentation only; no source was produced |
-| `NotProgramming/ERPM` | runnable | builds the report with latexmk |
-| `NotProgramming/ESDP1` | runnable | builds the report with latexmk |
-| `NotProgramming/ESOEN` | runnable | builds the report with latexmk |
-| `NotProgramming/MOM` | runnable | builds the report with latexmk |
-| `NotProgramming/SDM` | runnable | builds the report with latexmk |
-| `NotProgramming/SDM2` | runnable | builds the report with latexmk |
-| `NotProgramming/SPD` | runnable | builds the report with latexmk |
-| `NotProgramming/STUP` | runnable | npm install && npm start |
-| `Programming/AIS` | runnable | builds the report with latexmk |
-| `Programming/BD2` | runnable | npm install && npm start |
-| `Programming/BSS` | runnable | builds the report with latexmk |
-| `Programming/EADS` | runnable | compiles each standalone .cpp |
-| `Programming/EARIN` | runnable | python3 the entry script |
-| `Programming/ECOAR` | runnable | make |
-| `Programming/ECOTE` | runnable | python3 /home/kuhy/WUT_Computer_Science/Programming/ECOTE/program/code/main.py |
-| `Programming/ECRYPT` | no-code | test1.py is a 0-byte stub; this course produced no submitted code |
-| `Programming/ECRYPT_PROJECT` | runnable | python3 /home/kuhy/WUT_Computer_Science/Programming/ECRYPT_PROJECT/code |
-| `Programming/EGUI` | runnable | dotnet build |
-| `Programming/EOOP` | runnable | compiles each standalone .cpp |
-| `Programming/EOPSY` | runnable | make |
-| `Programming/EPFU` | runnable | make |
-| `Programming/E-PSYOPS` | runnable | opens in the Unity editor |
-| `Programming/ERSMS-project` | runnable | docker compose up |
-| `Programming/NLP` | runnable | python3 the entry script |
-| `Programming/PBAD` | runnable | jupyter nbconvert --execute |
-| `Programming/PORR` | runnable | python3 /home/kuhy/WUT_Computer_Science/Programming/PORR/code |
-| `Programming/PSD` | runnable | starts the Kafka/Flink stack (installs packages with sudo) |
-| `Programming/psd_project` | runnable | mvn package |
-| `Programming/TRAK` | runnable | python3 the entry script |
-| `Programming/twm_4` | runnable | jupyter nbconvert --execute |
-| `Programming/USD` | no-code | README describes a MetaDrive RL assignment; no implementation was submitted |
-| `Programming/WDWR` | runnable | python3 the entry script |
-| `Programming/ZPOB` | runnable | builds the report with latexmk |
+`-` means that environment has not probed yet.
+
+| Course | local | ci | Detail (local) |
+|---|---|---|---|
+| `NotProgramming/DPZ` | runnable | - | builds the report with latexmk |
+| `NotProgramming/ECOGR` | runnable | - | builds the report with latexmk |
+| `NotProgramming/EDABA` | runnable | - | builds the report with latexmk |
+| `NotProgramming/EDABA-LAB` | runnable | - | builds the report with latexmk |
+| `NotProgramming/EDISE` | runnable | - | builds the report with latexmk |
+| `NotProgramming/EDISP` | no-code | - | submitted lab reports as PDFs only; no source was produced |
+| `NotProgramming/ELAC` | runnable | - | builds the report with latexmk |
+| `NotProgramming/EMISY` | runnable | - | builds the report with latexmk |
+| `NotProgramming/ENUME` | blocked | - | needs MATLAB or Octave (none of: matlab octave) |
+| `NotProgramming/EPHY2` | no-code | - | submitted lab reports as PDFs only; no source was produced |
+| `NotProgramming/EPOGAI` | no-code | - | a presentation only; no source was produced |
+| `NotProgramming/ERPM` | runnable | - | builds the report with latexmk |
+| `NotProgramming/ESDP1` | runnable | - | builds the report with latexmk |
+| `NotProgramming/ESOEN` | runnable | - | builds the report with latexmk |
+| `NotProgramming/MOM` | runnable | - | builds the report with latexmk |
+| `NotProgramming/SDM` | runnable | - | builds the report with latexmk |
+| `NotProgramming/SDM2` | runnable | - | builds the report with latexmk |
+| `NotProgramming/SPD` | runnable | - | builds the report with latexmk |
+| `NotProgramming/STUP` | runnable | - | npm install && npm start |
+| `Programming/AIS` | runnable | - | builds the report with latexmk |
+| `Programming/BD2` | runnable | - | npm install && npm start |
+| `Programming/BSS` | runnable | - | builds the report with latexmk |
+| `Programming/EADS` | runnable | - | compiles each standalone .cpp |
+| `Programming/EARIN` | runnable | - | python3 the entry script |
+| `Programming/ECOAR` | runnable | - | make |
+| `Programming/ECOTE` | runnable | - | python3 /home/kuhy/WUT_Computer_Science/Programming/ECOTE/program/translator/main.py |
+| `Programming/ECRYPT` | no-code | - | test1.py is a 0-byte stub; this course produced no submitted code |
+| `Programming/ECRYPT_PROJECT` | runnable | - | python3 /home/kuhy/WUT_Computer_Science/Programming/ECRYPT_PROJECT/code |
+| `Programming/EGUI` | runnable | - | dotnet build |
+| `Programming/EOOP` | runnable | - | compiles each standalone .cpp |
+| `Programming/EOPSY` | runnable | - | make |
+| `Programming/EPFU` | runnable | - | make |
+| `Programming/E-PSYOPS` | runnable | - | opens in the Unity editor |
+| `Programming/ERSMS-project` | runnable | - | docker compose up |
+| `Programming/NLP` | runnable | - | python3 the entry script |
+| `Programming/PBAD` | runnable | - | jupyter nbconvert --execute |
+| `Programming/PORR` | runnable | - | python3 /home/kuhy/WUT_Computer_Science/Programming/PORR/code |
+| `Programming/PSD` | runnable | - | starts the Kafka/Flink stack (installs packages with sudo) |
+| `Programming/psd_project` | runnable | - | mvn package |
+| `Programming/TRAK` | runnable | - | python3 the entry script |
+| `Programming/twm_4` | runnable | - | jupyter nbconvert --execute |
+| `Programming/USD` | no-code | - | README describes a MetaDrive RL assignment; no implementation was submitted |
+| `Programming/WDWR` | runnable | - | python3 the entry script |
+| `Programming/ZPOB` | runnable | - | builds the report with latexmk |
diff --git a/Programming/ECOTE/program/README.md b/Programming/ECOTE/program/README.md
index 9c884d58..97efcb58 100644
--- a/Programming/ECOTE/program/README.md
+++ b/Programming/ECOTE/program/README.md
@@ -1,9 +1,21 @@
-To run unit tests:
-Enter project environment:
+# ECOTE translator
-source /ecote/bin/activate
+Translates LaTeX `tabular` environments into HTML tables.
-Run tests:
+| Path | What it is |
+|---|---|
+| `translator/` | The implementation. Entry point `translator/main.py`. |
+| `translator/functional_tests/` | End-to-end fixtures. |
+| `unit_tests/test_code/` | 17 pytest modules. |
+## Run the tests
+
+```bash
python -m pytest unit_tests
+```
+> The package used to be called `code/`, which shadows Python's standard
+> library `code` module. That is not cosmetic: pytest's debugging plugin
+> imports `pdb`, `pdb` imports `code`, and it got this directory instead, so
+> the whole suite died with an INTERNALERROR before collecting a single test.
+> Renaming it is what makes these tests runnable at all.
diff --git a/Programming/ECOTE/program/code/.flake8 b/Programming/ECOTE/program/translator/.flake8
similarity index 100%
rename from Programming/ECOTE/program/code/.flake8
rename to Programming/ECOTE/program/translator/.flake8
diff --git a/Programming/ECOTE/program/code/.pylintrc b/Programming/ECOTE/program/translator/.pylintrc
similarity index 100%
rename from Programming/ECOTE/program/code/.pylintrc
rename to Programming/ECOTE/program/translator/.pylintrc
diff --git a/Programming/ECOTE/program/code/__init__.py b/Programming/ECOTE/program/translator/__init__.py
similarity index 100%
rename from Programming/ECOTE/program/code/__init__.py
rename to Programming/ECOTE/program/translator/__init__.py
diff --git a/Programming/ECOTE/program/code/functional_tests/ERRORwrongColumnsNumber/texfilecolumnswrong.tex b/Programming/ECOTE/program/translator/functional_tests/ERRORwrongColumnsNumber/texfilecolumnswrong.tex
similarity index 100%
rename from Programming/ECOTE/program/code/functional_tests/ERRORwrongColumnsNumber/texfilecolumnswrong.tex
rename to Programming/ECOTE/program/translator/functional_tests/ERRORwrongColumnsNumber/texfilecolumnswrong.tex
diff --git a/Programming/ECOTE/program/code/functional_tests/ERRORwrongColumnsNumber/texfilecolumnswrong.tex.html b/Programming/ECOTE/program/translator/functional_tests/ERRORwrongColumnsNumber/texfilecolumnswrong.tex.html
similarity index 100%
rename from Programming/ECOTE/program/code/functional_tests/ERRORwrongColumnsNumber/texfilecolumnswrong.tex.html
rename to Programming/ECOTE/program/translator/functional_tests/ERRORwrongColumnsNumber/texfilecolumnswrong.tex.html
diff --git a/Programming/ECOTE/program/code/functional_tests/PASScomplicatedTable/texfilelonger.pdf b/Programming/ECOTE/program/translator/functional_tests/PASScomplicatedTable/texfilelonger.pdf
similarity index 100%
rename from Programming/ECOTE/program/code/functional_tests/PASScomplicatedTable/texfilelonger.pdf
rename to Programming/ECOTE/program/translator/functional_tests/PASScomplicatedTable/texfilelonger.pdf
diff --git a/Programming/ECOTE/program/code/functional_tests/PASScomplicatedTable/texfilelonger.tex b/Programming/ECOTE/program/translator/functional_tests/PASScomplicatedTable/texfilelonger.tex
similarity index 100%
rename from Programming/ECOTE/program/code/functional_tests/PASScomplicatedTable/texfilelonger.tex
rename to Programming/ECOTE/program/translator/functional_tests/PASScomplicatedTable/texfilelonger.tex
diff --git a/Programming/ECOTE/program/code/functional_tests/PASScomplicatedTable/texfilelonger.tex.html b/Programming/ECOTE/program/translator/functional_tests/PASScomplicatedTable/texfilelonger.tex.html
similarity index 100%
rename from Programming/ECOTE/program/code/functional_tests/PASScomplicatedTable/texfilelonger.tex.html
rename to Programming/ECOTE/program/translator/functional_tests/PASScomplicatedTable/texfilelonger.tex.html
diff --git a/Programming/ECOTE/program/code/functional_tests/PASSsimpleMultipleTables/texfile.pdf b/Programming/ECOTE/program/translator/functional_tests/PASSsimpleMultipleTables/texfile.pdf
similarity index 100%
rename from Programming/ECOTE/program/code/functional_tests/PASSsimpleMultipleTables/texfile.pdf
rename to Programming/ECOTE/program/translator/functional_tests/PASSsimpleMultipleTables/texfile.pdf
diff --git a/Programming/ECOTE/program/code/functional_tests/PASSsimpleMultipleTables/texfile.tex b/Programming/ECOTE/program/translator/functional_tests/PASSsimpleMultipleTables/texfile.tex
similarity index 100%
rename from Programming/ECOTE/program/code/functional_tests/PASSsimpleMultipleTables/texfile.tex
rename to Programming/ECOTE/program/translator/functional_tests/PASSsimpleMultipleTables/texfile.tex
diff --git a/Programming/ECOTE/program/code/functional_tests/PASSsimpleMultipleTables/texfile.tex.html b/Programming/ECOTE/program/translator/functional_tests/PASSsimpleMultipleTables/texfile.tex.html
similarity index 100%
rename from Programming/ECOTE/program/code/functional_tests/PASSsimpleMultipleTables/texfile.tex.html
rename to Programming/ECOTE/program/translator/functional_tests/PASSsimpleMultipleTables/texfile.tex.html
diff --git a/Programming/ECOTE/program/code/functional_tests/PassparametersTable/texfilelonger.pdf b/Programming/ECOTE/program/translator/functional_tests/PassparametersTable/texfilelonger.pdf
similarity index 100%
rename from Programming/ECOTE/program/code/functional_tests/PassparametersTable/texfilelonger.pdf
rename to Programming/ECOTE/program/translator/functional_tests/PassparametersTable/texfilelonger.pdf
diff --git a/Programming/ECOTE/program/code/functional_tests/PassparametersTable/texfilelonger.tex b/Programming/ECOTE/program/translator/functional_tests/PassparametersTable/texfilelonger.tex
similarity index 100%
rename from Programming/ECOTE/program/code/functional_tests/PassparametersTable/texfilelonger.tex
rename to Programming/ECOTE/program/translator/functional_tests/PassparametersTable/texfilelonger.tex
diff --git a/Programming/ECOTE/program/code/functional_tests/PassparametersTable/texfilelonger.tex.html b/Programming/ECOTE/program/translator/functional_tests/PassparametersTable/texfilelonger.tex.html
similarity index 100%
rename from Programming/ECOTE/program/code/functional_tests/PassparametersTable/texfilelonger.tex.html
rename to Programming/ECOTE/program/translator/functional_tests/PassparametersTable/texfilelonger.tex.html
diff --git a/Programming/ECOTE/program/code/main.py b/Programming/ECOTE/program/translator/main.py
similarity index 100%
rename from Programming/ECOTE/program/code/main.py
rename to Programming/ECOTE/program/translator/main.py
diff --git a/Programming/ECOTE/program/code/texfile.tex b/Programming/ECOTE/program/translator/texfile.tex
similarity index 100%
rename from Programming/ECOTE/program/code/texfile.tex
rename to Programming/ECOTE/program/translator/texfile.tex
diff --git a/Programming/ECOTE/program/code/texfile.tex.html b/Programming/ECOTE/program/translator/texfile.tex.html
similarity index 100%
rename from Programming/ECOTE/program/code/texfile.tex.html
rename to Programming/ECOTE/program/translator/texfile.tex.html
diff --git a/Programming/ECOTE/program/unit_tests/test_code/test_begin_document.py b/Programming/ECOTE/program/unit_tests/test_code/test_begin_document.py
index 12c21341..ae404221 100644
--- a/Programming/ECOTE/program/unit_tests/test_code/test_begin_document.py
+++ b/Programming/ECOTE/program/unit_tests/test_code/test_begin_document.py
@@ -1,7 +1,7 @@
"""
Tests begin document function
"""
-from code.main import begin_document
+from translator.main import begin_document
# Write python tests for a function translating LaTeX documentclass to html
def given_empty_then_error():
diff --git a/Programming/ECOTE/program/unit_tests/test_code/test_begin_tabular.py b/Programming/ECOTE/program/unit_tests/test_code/test_begin_tabular.py
index dd02bd7a..91d6ae32 100644
--- a/Programming/ECOTE/program/unit_tests/test_code/test_begin_tabular.py
+++ b/Programming/ECOTE/program/unit_tests/test_code/test_begin_tabular.py
@@ -1,7 +1,7 @@
"""
Tests begin document function
"""
-from code.main import begin_tabular
+from translator.main import begin_tabular
# Write python tests for a function translating LaTeX documentclass to html
def given_empty_then_error():
diff --git a/Programming/ECOTE/program/unit_tests/test_code/test_command_name_check.py b/Programming/ECOTE/program/unit_tests/test_code/test_command_name_check.py
index b5fe891d..57d72abd 100644
--- a/Programming/ECOTE/program/unit_tests/test_code/test_command_name_check.py
+++ b/Programming/ECOTE/program/unit_tests/test_code/test_command_name_check.py
@@ -1,4 +1,4 @@
-from code.main import command_name_check
+from translator.main import command_name_check
def given_valid_then_empty():
diff --git a/Programming/ECOTE/program/unit_tests/test_code/test_document_class.py b/Programming/ECOTE/program/unit_tests/test_code/test_document_class.py
index 94520315..a06d6770 100644
--- a/Programming/ECOTE/program/unit_tests/test_code/test_document_class.py
+++ b/Programming/ECOTE/program/unit_tests/test_code/test_document_class.py
@@ -1,7 +1,7 @@
"""
Tests document class function
"""
-from code.main import document_class
+from translator.main import document_class
# Write python tests for a function translating LaTeX documentclass to html
diff --git a/Programming/ECOTE/program/unit_tests/test_code/test_document_class_only_checks.py b/Programming/ECOTE/program/unit_tests/test_code/test_document_class_only_checks.py
index ad8aa299..b85f22ea 100644
--- a/Programming/ECOTE/program/unit_tests/test_code/test_document_class_only_checks.py
+++ b/Programming/ECOTE/program/unit_tests/test_code/test_document_class_only_checks.py
@@ -1,4 +1,4 @@
-from code.main import document_class_only_checks
+from translator.main import document_class_only_checks
def test_missing_curly_bracket():
diff --git a/Programming/ECOTE/program/unit_tests/test_code/test_generic_checks.py b/Programming/ECOTE/program/unit_tests/test_code/test_generic_checks.py
index 545c4d46..454ebe95 100644
--- a/Programming/ECOTE/program/unit_tests/test_code/test_generic_checks.py
+++ b/Programming/ECOTE/program/unit_tests/test_code/test_generic_checks.py
@@ -1,4 +1,4 @@
-from code.main import generic_checks
+from translator.main import generic_checks
def test_empty_string():
diff --git a/Programming/ECOTE/program/unit_tests/test_code/test_generic_checks_command.py b/Programming/ECOTE/program/unit_tests/test_code/test_generic_checks_command.py
index f4793538..cd15fba0 100644
--- a/Programming/ECOTE/program/unit_tests/test_code/test_generic_checks_command.py
+++ b/Programming/ECOTE/program/unit_tests/test_code/test_generic_checks_command.py
@@ -1,4 +1,4 @@
-from code.main import generic_checks_command
+from translator.main import generic_checks_command
def test_empty_string():
diff --git a/Programming/ECOTE/program/unit_tests/test_code/test_length_conversions.py b/Programming/ECOTE/program/unit_tests/test_code/test_length_conversions.py
index 14229661..cedcb55f 100644
--- a/Programming/ECOTE/program/unit_tests/test_code/test_length_conversions.py
+++ b/Programming/ECOTE/program/unit_tests/test_code/test_length_conversions.py
@@ -1,7 +1,7 @@
"""
Tests begin document function
"""
-from code.main import length_conversions
+from translator.main import length_conversions
# Write python tests for a function translating LaTeX documentclass to html
diff --git a/Programming/ECOTE/program/unit_tests/test_code/test_only_pipes_and_space.py b/Programming/ECOTE/program/unit_tests/test_code/test_only_pipes_and_space.py
index c330c426..d6d7024f 100644
--- a/Programming/ECOTE/program/unit_tests/test_code/test_only_pipes_and_space.py
+++ b/Programming/ECOTE/program/unit_tests/test_code/test_only_pipes_and_space.py
@@ -1,4 +1,4 @@
-from code.main import only_pipes_and_space
+from translator.main import only_pipes_and_space
def given_empty_then_true():
diff --git a/Programming/ECOTE/program/unit_tests/test_code/test_split_columns.py b/Programming/ECOTE/program/unit_tests/test_code/test_split_columns.py
index 30f6012d..48dfc59a 100644
--- a/Programming/ECOTE/program/unit_tests/test_code/test_split_columns.py
+++ b/Programming/ECOTE/program/unit_tests/test_code/test_split_columns.py
@@ -1,4 +1,4 @@
-from code.main import split_columns
+from translator.main import split_columns
def given_empty_then_correct():
diff --git a/Programming/ECOTE/program/unit_tests/test_code/test_split_rows.py b/Programming/ECOTE/program/unit_tests/test_code/test_split_rows.py
index 98c9e43e..3424ee8c 100644
--- a/Programming/ECOTE/program/unit_tests/test_code/test_split_rows.py
+++ b/Programming/ECOTE/program/unit_tests/test_code/test_split_rows.py
@@ -1,4 +1,4 @@
-from code.main import split_rows
+from translator.main import split_rows
def given_empty_then_correct():
diff --git a/Programming/ECOTE/program/unit_tests/test_code/test_tabular_columns_parameters.py b/Programming/ECOTE/program/unit_tests/test_code/test_tabular_columns_parameters.py
index 07a91130..f289bdfd 100644
--- a/Programming/ECOTE/program/unit_tests/test_code/test_tabular_columns_parameters.py
+++ b/Programming/ECOTE/program/unit_tests/test_code/test_tabular_columns_parameters.py
@@ -1,4 +1,4 @@
-from code.main import tabular_columns_parameters
+from translator.main import tabular_columns_parameters
def given_empty_then_error():
diff --git a/Programming/ECOTE/program/unit_tests/test_code/test_tabular_parameters.py b/Programming/ECOTE/program/unit_tests/test_code/test_tabular_parameters.py
index eade0793..90a47d86 100644
--- a/Programming/ECOTE/program/unit_tests/test_code/test_tabular_parameters.py
+++ b/Programming/ECOTE/program/unit_tests/test_code/test_tabular_parameters.py
@@ -2,7 +2,7 @@
Tests tabular_parameters function
"""
-from code.main import tabular_parameters
+from translator.main import tabular_parameters
def given_empty_then_empty():
"""
diff --git a/Programming/ECOTE/program/unit_tests/test_code/test_tabular_required_parameters.py b/Programming/ECOTE/program/unit_tests/test_code/test_tabular_required_parameters.py
index fb6623ae..f700737c 100644
--- a/Programming/ECOTE/program/unit_tests/test_code/test_tabular_required_parameters.py
+++ b/Programming/ECOTE/program/unit_tests/test_code/test_tabular_required_parameters.py
@@ -2,7 +2,7 @@
Tests tabular_parameters function
"""
-from code.main import tabular_required_parameters
+from translator.main import tabular_required_parameters
def given_empty_then_error():
diff --git a/Programming/ECOTE/program/unit_tests/test_code/test_translate_column.py b/Programming/ECOTE/program/unit_tests/test_code/test_translate_column.py
index 493741af..a7861f19 100644
--- a/Programming/ECOTE/program/unit_tests/test_code/test_translate_column.py
+++ b/Programming/ECOTE/program/unit_tests/test_code/test_translate_column.py
@@ -1,4 +1,4 @@
-from code.main import translate_column
+from translator.main import translate_column
def given_empty_then_correct():
diff --git a/Programming/ECOTE/program/unit_tests/test_code/test_translate_inside_to_html.py b/Programming/ECOTE/program/unit_tests/test_code/test_translate_inside_to_html.py
index a949a1a5..9ac4f2c3 100644
--- a/Programming/ECOTE/program/unit_tests/test_code/test_translate_inside_to_html.py
+++ b/Programming/ECOTE/program/unit_tests/test_code/test_translate_inside_to_html.py
@@ -1,5 +1,5 @@
-from code.main import translate_inside_to_html
-from code.main import tabular_required_parameters
+from translator.main import translate_inside_to_html
+from translator.main import tabular_required_parameters
def given_correct_then_correct():
diff --git a/Programming/ECOTE/run.sh b/Programming/ECOTE/run.sh
index 2e0cb758..9252dc47 100755
--- a/Programming/ECOTE/run.sh
+++ b/Programming/ECOTE/run.sh
@@ -8,5 +8,5 @@ set -euo pipefail
. "$(dirname "$(readlink -f "$0")")/../../scripts/course_lib.sh"
course_main --kind python \
- --entry "$(dirname "$(readlink -f "$0")")/program/code/main.py" \
+ --entry "$(dirname "$(readlink -f "$0")")/program/translator/main.py" \
-- "$@"
diff --git a/gates/runnability-local.tsv b/gates/runnability-local.tsv
new file mode 100644
index 00000000..98cc7133
--- /dev/null
+++ b/gates/runnability-local.tsv
@@ -0,0 +1,44 @@
+NotProgramming/DPZ runnable builds the report with latexmk
+NotProgramming/ECOGR runnable builds the report with latexmk
+NotProgramming/EDABA runnable builds the report with latexmk
+NotProgramming/EDABA-LAB runnable builds the report with latexmk
+NotProgramming/EDISE runnable builds the report with latexmk
+NotProgramming/EDISP no-code submitted lab reports as PDFs only; no source was produced
+NotProgramming/ELAC runnable builds the report with latexmk
+NotProgramming/EMISY runnable builds the report with latexmk
+NotProgramming/ENUME blocked needs MATLAB or Octave (none of: matlab octave)
+NotProgramming/EPHY2 no-code submitted lab reports as PDFs only; no source was produced
+NotProgramming/EPOGAI no-code a presentation only; no source was produced
+NotProgramming/ERPM runnable builds the report with latexmk
+NotProgramming/ESDP1 runnable builds the report with latexmk
+NotProgramming/ESOEN runnable builds the report with latexmk
+NotProgramming/MOM runnable builds the report with latexmk
+NotProgramming/SDM runnable builds the report with latexmk
+NotProgramming/SDM2 runnable builds the report with latexmk
+NotProgramming/SPD runnable builds the report with latexmk
+NotProgramming/STUP runnable npm install && npm start
+Programming/AIS runnable builds the report with latexmk
+Programming/BD2 runnable npm install && npm start
+Programming/BSS runnable builds the report with latexmk
+Programming/EADS runnable compiles each standalone .cpp
+Programming/EARIN runnable python3 the entry script
+Programming/ECOAR runnable make
+Programming/ECOTE runnable python3 /home/kuhy/WUT_Computer_Science/Programming/ECOTE/program/translator/main.py
+Programming/ECRYPT no-code test1.py is a 0-byte stub; this course produced no submitted code
+Programming/ECRYPT_PROJECT runnable python3 /home/kuhy/WUT_Computer_Science/Programming/ECRYPT_PROJECT/code
+Programming/EGUI runnable dotnet build
+Programming/EOOP runnable compiles each standalone .cpp
+Programming/EOPSY runnable make
+Programming/EPFU runnable make
+Programming/E-PSYOPS runnable opens in the Unity editor
+Programming/ERSMS-project runnable docker compose up
+Programming/NLP runnable python3 the entry script
+Programming/PBAD runnable jupyter nbconvert --execute
+Programming/PORR runnable python3 /home/kuhy/WUT_Computer_Science/Programming/PORR/code
+Programming/PSD runnable starts the Kafka/Flink stack (installs packages with sudo)
+Programming/psd_project runnable mvn package
+Programming/TRAK runnable python3 the entry script
+Programming/twm_4 runnable jupyter nbconvert --execute
+Programming/USD no-code README describes a MetaDrive RL assignment; no implementation was submitted
+Programming/WDWR runnable python3 the entry script
+Programming/ZPOB runnable builds the report with latexmk
diff --git a/run.sh b/run.sh
index 2e26a599..1479532c 100755
--- a/run.sh
+++ b/run.sh
@@ -84,39 +84,56 @@ probe_one() {
cmd_status() {
local env_label="${CI:+ci}"
env_label="${env_label:-local}"
+ local store="${SCRIPT_DIR}/gates/runnability-${env_label}.tsv"
log "probing course directories (${env_label})"
- local tmp
- tmp="$(mktemp)"
- trap 'rm -f "${tmp:-}"' RETURN
-
local dir rel line
+ : > "$store"
while IFS= read -r dir; do
rel="${dir#"${SCRIPT_DIR}"/}"
line="$(probe_one "$dir")"
- printf '%s\t%s\n' "$rel" "$line" >> "$tmp"
+ printf '%s\t%s\n' "$rel" "$line" >> "$store"
done < <(course_dirs)
- write_runnability_doc "$tmp" "$env_label"
+ # Each environment writes only its OWN file, then the doc is rendered from
+ # both. Writing a single shared column would mean a CI run silently
+ # overwrote the local results with its own -- and the whole point of
+ # recording per environment is that "runnable here, blocked on a runner"
+ # is the normal case rather than drift.
+ write_runnability_doc
log "wrote ${RUNNABILITY_DOC##*/}"
- awk -F'\t' '{ n[$2]++ } END { for (k in n) printf " %-10s %d\n", k, n[k] }' "$tmp"
+ awk -F'\t' '{ n[$2]++ } END { for (k in n) printf " %-10s %d\n", k, n[k] }' "$store"
}
write_runnability_doc() {
- local data="$1" env_label="$2"
+ local local_tsv="${SCRIPT_DIR}/gates/runnability-local.tsv"
+ local ci_tsv="${SCRIPT_DIR}/gates/runnability-ci.tsv"
+ [[ -f "$local_tsv" ]] || : > "$local_tsv"
+ [[ -f "$ci_tsv" ]] || : > "$ci_tsv"
+
{
cat <<'MD'
# Runnability
Generated by `./run.sh status`. Do not edit by hand.
-Status is recorded PER ENVIRONMENT. A directory that is `runnable` locally and
+Status is recorded PER ENVIRONMENT, from `gates/runnability-local.tsv` and
+`gates/runnability-ci.tsv`. A directory that is `runnable` locally and
`blocked` in CI is the normal case, not drift: the runner has no MATLAB, no
-Unity and no .NET SDK. CI compares only its own column.
+Unity and no .NET SDK. Each environment rewrites only its own file, so a CI
+run cannot clobber the local column.
+
+`-` means that environment has not probed yet.
MD
- printf '| Course | %s | Detail |\n|---|---|---|\n' "$env_label"
- awk -F'\t' '{ printf "| `%s` | %s | %s |\n", $1, $2, $3 }' "$data"
+ printf '| Course | local | ci | Detail (local) |\n|---|---|---|---|\n'
+ # Compare FILENAME rather than using the NR==FNR idiom: with an EMPTY
+ # first file awk never reads it, so NR==FNR stays true for every row
+ # of the second file and the whole table comes out blank.
+ awk -F'\t' -v cifile="$ci_tsv" '
+ FILENAME == cifile { ci[$1] = $2; next }
+ { printf "| `%s` | %s | %s | %s |\n", $1, $2, ($1 in ci ? ci[$1] : "-"), $3 }
+ ' "$ci_tsv" "$local_tsv"
} > "$RUNNABILITY_DOC"
}
diff --git a/scripts/check_ignore_consistency.sh b/scripts/check_ignore_consistency.sh
new file mode 100755
index 00000000..c048175f
--- /dev/null
+++ b/scripts/check_ignore_consistency.sh
@@ -0,0 +1,42 @@
+#!/bin/bash
+
+# ============================================================================
+# Fail if any TRACKED file is also matched by a .gitignore rule.
+#
+# That combination fails silently: `git add` on such a file is not an error,
+# the edit simply never lands. This repo had 130 of them -- dist/ swallowing
+# the vendored jQuery bundle, and the LaTeX template's man-page patterns
+# (*.[1-9]) eating EPFU's date-named doc/21.11/ directory along with its
+# report.pdf.
+#
+# They were fixed by hand once. This is the gate that keeps them fixed, so
+# that .binary-allowlist can stop *instructing* the reader to re-run a command
+# and just have it enforced instead.
+#
+# Usage: scripts/check_ignore_consistency.sh
+# ============================================================================
+
+set -euo pipefail
+
+main() {
+ local offenders
+ offenders="$(git ls-files | git check-ignore --stdin --no-index 2>/dev/null || true)"
+
+ if [[ -z "$offenders" ]]; then
+ exit 0
+ fi
+
+ echo "Tracked files that .gitignore also matches:" >&2
+ printf '%s\n' "$offenders" | sed 's/^/ /' >&2
+ cat >&2 <<'MSG'
+
+Each of these fails SILENTLY on `git add` -- not an error, the edit just
+never lands. Fix by either:
+ 1. adding a "!" override in .gitignore (if the file belongs in the repo), or
+ 2. `git rm --cached` it (if it is build output that should never have been
+ tracked; --cached keeps it on disk).
+MSG
+ exit 1
+}
+
+main "$@"
diff --git a/scripts/course_lib.sh b/scripts/course_lib.sh
index 8b238f81..29e46eec 100644
--- a/scripts/course_lib.sh
+++ b/scripts/course_lib.sh
@@ -140,11 +140,19 @@ _run() {
[[ -n "$tex" ]] || _no_code "no .tex found"
local tool
tool="$(_require_any 'a LaTeX toolchain' latexmk pdflatex xelatex)"
+ # Build into build/, never beside the source. A submitted report.pdf
+ # is a tracked deliverable, and a rebuild whose output name happens
+ # to match it would silently overwrite what was handed in.
+ local dir base
+ dir="$(dirname "$tex")"
+ base="$(basename "$tex")"
if [[ $tool == latexmk ]]; then
- ( cd "$(dirname "$tex")" && latexmk -pdf -interaction=nonstopmode "$(basename "$tex")" )
+ ( cd "$dir" && latexmk -pdf -interaction=nonstopmode -outdir=build "$base" )
else
- ( cd "$(dirname "$tex")" && "$tool" -interaction=nonstopmode "$(basename "$tex")" )
+ ( cd "$dir" && mkdir -p build \
+ && "$tool" -interaction=nonstopmode -output-directory=build "$base" )
fi
+ printf 'output in %s/build\n' "$dir"
;;
python)
local entry="${COURSE_ENTRY:-}"