|
1 | | -""" |
2 | | -Extract scheduling functions directly from the workflow pre-step heredoc. |
3 | | -
|
4 | | -Instead of duplicating the workflow's JavaScript code in a separate module, we parse |
5 | | -workflows/autoloop.md, extract the JavaScript heredoc, write the function definitions |
6 | | -to a temp CommonJS module, and call them via Node.js subprocess. |
| 1 | +"""Test fixtures for the standalone Autoloop scheduler. |
7 | 2 |
|
8 | | -This ensures tests always run against the actual workflow code. |
| 3 | +The scheduler logic lives in ``workflows/scripts/autoloop_scheduler.py`` and is |
| 4 | +also distributed at ``.github/workflows/scripts/autoloop_scheduler.py`` (the |
| 5 | +dogfooded deploy copy). Tests import the source module directly via importlib. |
9 | 6 | """ |
10 | 7 |
|
11 | | -import json |
| 8 | +import importlib.util |
12 | 9 | import os |
13 | | -import re |
14 | | -import subprocess |
15 | | -import tempfile |
16 | | -from datetime import timedelta |
17 | | - |
18 | | -WORKFLOW_PATH = os.path.join(os.path.dirname(__file__), "..", "workflows", "autoloop.md") |
19 | | - |
20 | | -# Path to the extracted JS module |
21 | | -_JS_MODULE_PATH = os.path.join(tempfile.gettempdir(), "autoloop_test_functions.cjs") |
22 | | - |
23 | | - |
24 | | -def _load_workflow_functions(): |
25 | | - """Parse workflows/autoloop.md and extract JS function defs from the pre-step.""" |
26 | | - with open(WORKFLOW_PATH) as f: |
27 | | - content = f.read() |
28 | | - |
29 | | - # Extract the JavaScript heredoc between JSEOF markers |
30 | | - m = re.search(r"node - << 'JSEOF'\n(.*?)\n\s*JSEOF", content, re.DOTALL) |
31 | | - assert m, "Could not find JSEOF heredoc in workflows/autoloop.md" |
32 | | - source = m.group(1) |
33 | | - |
34 | | - # Extract function definitions: everything up to the main() async function. |
35 | | - # Functions are defined before 'async function main()' |
36 | | - lines = source.split("\n") |
37 | | - func_lines = [] |
38 | | - for line in lines: |
39 | | - if line.strip().startswith("async function main"): |
40 | | - break |
41 | | - func_lines.append(line) |
42 | | - |
43 | | - func_source = "\n".join(func_lines) |
44 | | - |
45 | | - # Write to a temp .cjs file with module.exports |
46 | | - with open(_JS_MODULE_PATH, "w") as f: |
47 | | - f.write(func_source) |
48 | | - f.write( |
49 | | - "\n\nmodule.exports = " |
50 | | - "{ parseMachineState, parseSchedule, getProgramName, readProgramState, parseLinkHeader };\n" |
51 | | - ) |
52 | | - |
53 | | - return True |
54 | | - |
55 | | - |
56 | | -def _call_js(func_name, *args): |
57 | | - """Call a JS function from the extracted workflow module and return the result.""" |
58 | | - args_json = json.dumps(list(args)) |
59 | | - escaped_path = json.dumps(_JS_MODULE_PATH) |
60 | | - script = ( |
61 | | - "const m = require(" + escaped_path + ");\n" |
62 | | - "const result = m." + func_name + "(..." + args_json + ");\n" |
63 | | - "process.stdout.write(JSON.stringify(result === undefined ? null : result));\n" |
64 | | - ) |
65 | | - result = subprocess.run( |
66 | | - ["node", "-e", script], |
67 | | - capture_output=True, |
68 | | - text=True, |
69 | | - timeout=10, |
| 10 | +import sys |
| 11 | + |
| 12 | +# Path to the standalone scheduler script (source-of-truth lives in workflows/). |
| 13 | +SCHEDULER_PATH = os.path.normpath( |
| 14 | + os.path.join( |
| 15 | + os.path.dirname(__file__), |
| 16 | + "..", |
| 17 | + "workflows", |
| 18 | + "scripts", |
| 19 | + "autoloop_scheduler.py", |
70 | 20 | ) |
71 | | - if result.returncode != 0: |
72 | | - raise RuntimeError("Node.js error calling " + func_name + ": " + result.stderr) |
73 | | - if not result.stdout.strip(): |
74 | | - return None |
75 | | - return json.loads(result.stdout) |
76 | | - |
77 | | - |
78 | | -# Initialize at import time |
79 | | -_load_workflow_functions() |
80 | | - |
81 | | - |
82 | | -def _parse_schedule_wrapper(s): |
83 | | - """Python wrapper for JS parseSchedule. Converts milliseconds to timedelta.""" |
84 | | - ms = _call_js("parseSchedule", s) |
85 | | - if ms is None: |
86 | | - return None |
87 | | - return timedelta(milliseconds=ms) |
| 21 | +) |
88 | 22 |
|
89 | | - |
90 | | -def _parse_machine_state_wrapper(content): |
91 | | - """Python wrapper for JS parseMachineState.""" |
92 | | - return _call_js("parseMachineState", content) |
93 | | - |
94 | | - |
95 | | -def _get_program_name_wrapper(pf): |
96 | | - """Python wrapper for JS getProgramName.""" |
97 | | - return _call_js("getProgramName", pf) |
| 23 | +_spec = importlib.util.spec_from_file_location("autoloop_scheduler", SCHEDULER_PATH) |
| 24 | +autoloop_scheduler = importlib.util.module_from_spec(_spec) |
| 25 | +sys.modules["autoloop_scheduler"] = autoloop_scheduler |
| 26 | +_spec.loader.exec_module(autoloop_scheduler) |
98 | 27 |
|
99 | 28 |
|
| 29 | +# Backwards-compatible function map (mirrors the previous JS-extracting conftest). |
100 | 30 | _funcs = { |
101 | | - "parse_schedule": _parse_schedule_wrapper, |
102 | | - "parse_machine_state": _parse_machine_state_wrapper, |
103 | | - "get_program_name": _get_program_name_wrapper, |
104 | | - "read_program_state": lambda name: _call_js("readProgramState", name), |
105 | | - "parse_link_header": lambda header: _call_js("parseLinkHeader", header), |
| 31 | + "parse_schedule": autoloop_scheduler.parse_schedule, |
| 32 | + "parse_machine_state": autoloop_scheduler.parse_machine_state, |
| 33 | + "get_program_name": autoloop_scheduler.get_program_name, |
| 34 | + "read_program_state": autoloop_scheduler.read_program_state, |
| 35 | + "parse_link_header": autoloop_scheduler.parse_link_header, |
106 | 36 | } |
107 | | - |
108 | | - |
109 | | -def _extract_inline_pattern(name): |
110 | | - """Extract the JavaScript heredoc source from the workflow. |
111 | | -
|
112 | | - This is a helper for inspecting the full inline source if needed. |
113 | | - """ |
114 | | - with open(WORKFLOW_PATH) as f: |
115 | | - content = f.read() |
116 | | - m = re.search(r"node - << 'JSEOF'\n(.*?)\n\s*JSEOF", content, re.DOTALL) |
117 | | - return m.group(1) if m else "" |
0 commit comments