Skip to main content

ignition_core/webdev/
testing.rs

1//! The embedded TESTING bundle (ADOPT-04, the D2 landing) — the Jython
2//! test framework + its two WebDev routes, ported OUT of
3//! agentic-ignition-tooling's `scaffold-testing.sh` (1 806-line shell
4//! scaffolder) into the repo as data — the `ROUTE_FILES` precedent,
5//! byte-identical semantics, no shell, no Windows problem.
6//!
7//! ## What ships (20 members, one import)
8//!
9//! - `ignition/script-python/resources/testing/<module>/` — the five
10//!   framework modules (runner, assertions, decorators, helpers,
11//!   reporter) + `__tests__/` (the permanent smoke sentinel — the
12//!   empty-suite-trap answer: discover ALWAYS has ≥1 module and
13//!   `run_all` proves the chain live).
14//! - `com.inductiveautomation.webdev/resources/testing/{run,tags}/` —
15//!   the WebDev endpoints (`run` = discover/execute, `tags` =
16//!   e2e tag fixtures; `tags` is require-auth, `run` is open at the
17//!   WebDev layer like the cli routes).
18//!
19//! ## The relocation, retired
20//!
21//! The scaffolder wrote the 8.1 layout (`ignition/script-python/
22//! testing/…` — modules directly under the collection) and every 8.3
23//! deploy needed the relocation patch. The bundle here IS the 8.3
24//! resource-folder layout (`…/script-python/resources/testing/…` —
25//! live-verified on-disk spelling, 8.3.6 rig), and the runner's
26//! discovery walk bases its dotted module paths on the `resources`
27//! container when present (8.3) falling back to the bare collection
28//! (8.1) — one walk, both layouts, no per-deploy patch.
29//!
30//! ## Markers (substituted at pack time, the scriptExec pattern)
31//!
32//! - `__IGN_CLI_PROJECT__` — the deploy project name (the runner's
33//!   fallback constant + the run route's scoped-seeding lookup).
34//!   Exactly 2 sites; a marker-count test pins the total.
35//!
36//! Source of truth for the port: `.planning/research/ADOPT-RESEARCH.md`
37//! §D2 handoff (the manifest) + the extracted originals under
38//! `webdev/testing/` (this repo now owns them; the scaffolder is
39//! retired for ign workflows).
40
41/// The testing bundle — `(zip_member_path, contents)` pairs, the
42/// Designer-native 8.3 layout. Members are packed AFTER
43/// [`super::ROUTE_FILES`] by
44/// [`crate::client::webdev::build_deploy_zip`] when testing rides the
45/// deploy.
46pub const TESTING_FILES: &[(&str, &str)] = &[
47    // --- The Jython framework (ignition/script-python) ---------------
48    (
49        "ignition/script-python/resources/testing/runner/code.py",
50        include_str!(
51            "../../webdev/testing/ignition/script-python/resources/testing/runner/code.py"
52        ),
53    ),
54    (
55        "ignition/script-python/resources/testing/runner/resource.json",
56        include_str!(
57            "../../webdev/testing/ignition/script-python/resources/testing/runner/resource.json"
58        ),
59    ),
60    (
61        "ignition/script-python/resources/testing/assertions/code.py",
62        include_str!(
63            "../../webdev/testing/ignition/script-python/resources/testing/assertions/code.py"
64        ),
65    ),
66    (
67        "ignition/script-python/resources/testing/assertions/resource.json",
68        include_str!(
69            "../../webdev/testing/ignition/script-python/resources/testing/assertions/resource.json"
70        ),
71    ),
72    (
73        "ignition/script-python/resources/testing/decorators/code.py",
74        include_str!(
75            "../../webdev/testing/ignition/script-python/resources/testing/decorators/code.py"
76        ),
77    ),
78    (
79        "ignition/script-python/resources/testing/decorators/resource.json",
80        include_str!(
81            "../../webdev/testing/ignition/script-python/resources/testing/decorators/resource.json"
82        ),
83    ),
84    (
85        "ignition/script-python/resources/testing/helpers/code.py",
86        include_str!(
87            "../../webdev/testing/ignition/script-python/resources/testing/helpers/code.py"
88        ),
89    ),
90    (
91        "ignition/script-python/resources/testing/helpers/resource.json",
92        include_str!(
93            "../../webdev/testing/ignition/script-python/resources/testing/helpers/resource.json"
94        ),
95    ),
96    (
97        "ignition/script-python/resources/testing/reporter/code.py",
98        include_str!(
99            "../../webdev/testing/ignition/script-python/resources/testing/reporter/code.py"
100        ),
101    ),
102    (
103        "ignition/script-python/resources/testing/reporter/resource.json",
104        include_str!(
105            "../../webdev/testing/ignition/script-python/resources/testing/reporter/resource.json"
106        ),
107    ),
108    // The smoke sentinel — the empty-suite-trap answer.
109    (
110        "ignition/script-python/resources/testing/__tests__/code.py",
111        include_str!(
112            "../../webdev/testing/ignition/script-python/resources/testing/__tests__/code.py"
113        ),
114    ),
115    (
116        "ignition/script-python/resources/testing/__tests__/resource.json",
117        include_str!(
118            "../../webdev/testing/ignition/script-python/resources/testing/__tests__/resource.json"
119        ),
120    ),
121    // --- The WebDev routes --------------------------------------------
122    (
123        "com.inductiveautomation.webdev/resources/testing/run/doPost.py",
124        include_str!(
125            "../../webdev/testing/com.inductiveautomation.webdev/resources/testing/run/doPost.py"
126        ),
127    ),
128    (
129        "com.inductiveautomation.webdev/resources/testing/run/config.json",
130        include_str!(
131            "../../webdev/testing/com.inductiveautomation.webdev/resources/testing/run/config.json"
132        ),
133    ),
134    (
135        "com.inductiveautomation.webdev/resources/testing/run/resource.json",
136        include_str!(
137            "../../webdev/testing/com.inductiveautomation.webdev/resources/testing/run/resource.json"
138        ),
139    ),
140    (
141        "com.inductiveautomation.webdev/resources/testing/tags/doPost.py",
142        include_str!(
143            "../../webdev/testing/com.inductiveautomation.webdev/resources/testing/tags/doPost.py"
144        ),
145    ),
146    (
147        "com.inductiveautomation.webdev/resources/testing/tags/config.json",
148        include_str!(
149            "../../webdev/testing/com.inductiveautomation.webdev/resources/testing/tags/config.json"
150        ),
151    ),
152    (
153        "com.inductiveautomation.webdev/resources/testing/tags/resource.json",
154        include_str!(
155            "../../webdev/testing/com.inductiveautomation.webdev/resources/testing/tags/resource.json"
156        ),
157    ),
158];
159
160/// The deploy-time project marker (the `__IGN_CLI_SECRET__` pattern).
161pub(crate) const PROJECT_MARKER: &str = "__IGN_CLI_PROJECT__";
162
163/// The total marker count across the bundle — the pack substitution
164/// replaces every one; this pin makes an accidental template edit
165/// (adding or removing a marker) a test failure, not a runtime
166/// `PLACEHOLDER` on a gateway.
167pub(crate) const PROJECT_MARKER_COUNT: usize = 2;
168
169/// The testing bundle's route folder names (URL space:
170/// `/system/webdev/<project>/testing/<route>`).
171pub const TESTING_ROUTES: &[&str] = &["run", "tags"];
172
173#[cfg(test)]
174mod tests {
175    use super::{PROJECT_MARKER, PROJECT_MARKER_COUNT, TESTING_FILES, TESTING_ROUTES};
176
177    /// The bundle carries exactly the pinned marker count — the
178    /// substitution contract (every marker replaced at pack).
179    #[test]
180    fn project_marker_count_is_pinned() {
181        let total: usize = TESTING_FILES
182            .iter()
183            .map(|(_, contents)| contents.matches(PROJECT_MARKER).count())
184            .sum();
185        assert_eq!(
186            total, PROJECT_MARKER_COUNT,
187            "a testing-bundle edit moved the __IGN_CLI_PROJECT__ marker count"
188        );
189    }
190
191    /// The bundle is complete: 20 members, both route folders, all six
192    /// script modules — the D2 manifest pinned.
193    #[test]
194    fn bundle_manifest_is_pinned() {
195        assert_eq!(TESTING_FILES.len(), 18, "18 members: 12 script + 6 route");
196        for route in TESTING_ROUTES {
197            let prefix = format!("com.inductiveautomation.webdev/resources/testing/{route}/");
198            let files = TESTING_FILES
199                .iter()
200                .filter(|(name, _)| name.starts_with(&prefix))
201                .count();
202            assert_eq!(
203                files, 3,
204                "route {route} carries its 3 files (POST-only - the two-method config does not register on 8.3)"
205            );
206        }
207        for module in [
208            "runner",
209            "assertions",
210            "decorators",
211            "helpers",
212            "reporter",
213            "__tests__",
214        ] {
215            let path = format!("ignition/script-python/resources/testing/{module}/code.py");
216            assert!(
217                TESTING_FILES.iter().any(|(name, _)| *name == path),
218                "module {module} present"
219            );
220        }
221    }
222
223    /// The runner ships the 8.3 walk (the resources-container base with
224    /// the 8.1 fallback) — the relocation patch, retired IN the source.
225    #[test]
226    fn runner_carries_the_dual_layout_walk() {
227        let runner = TESTING_FILES
228            .iter()
229            .find(|(name, _)| *name == "ignition/script-python/resources/testing/runner/code.py")
230            .map(|(_, contents)| *contents)
231            .expect("runner present");
232        assert!(runner.contains("resources_path = File(script_path, \"resources\")"));
233        assert!(
234            runner.contains("base = resources_path if resources_path.exists() else script_path")
235        );
236    }
237}