scrollcase_consumer/
execution.rs1use std::collections::BTreeSet;
9
10use crate::contract::targets::BoxTargetAdapter;
11use crate::error::{fail, Result};
12use crate::path::safe_relative_path;
13use crate::release::Execution;
14
15fn python_major_minor(version: &str) -> Result<String> {
17 let mut parts = version.split('.');
18 let (Some(major), Some(minor)) = (parts.next(), parts.next()) else {
19 fail!("Invalid Python version for execution discovery: {version}.");
20 };
21 if major.is_empty()
22 || minor.is_empty()
23 || !major.bytes().all(|byte| byte.is_ascii_digit())
24 || !minor.bytes().all(|byte| byte.is_ascii_digit())
25 {
26 fail!("Invalid Python version for execution discovery: {version}.");
27 }
28 Ok(format!("{major}.{minor}"))
29}
30
31fn module_entry_points(
33 adapter: &BoxTargetAdapter,
34 module: &str,
35 python_version: &str,
36) -> Result<Vec<String>> {
37 let module_path = module.replace('.', "/");
38 let relative = [
39 format!("{module_path}.py"),
40 format!("{module_path}/__main__.py"),
41 ];
42 let standard_library = if adapter.platform == "windows" {
43 "venv/Lib".to_string()
44 } else {
45 format!("venv/lib/python{}", python_major_minor(python_version)?)
46 };
47 let roots = [
48 String::new(),
49 standard_library.clone(),
50 format!("{standard_library}/site-packages"),
51 ];
52 Ok(roots
53 .iter()
54 .flat_map(|root| {
55 relative.iter().map(move |candidate| {
56 if root.is_empty() {
57 candidate.clone()
58 } else {
59 format!("{root}/{candidate}")
60 }
61 })
62 })
63 .collect())
64}
65
66pub fn assert_execution_files(
75 execution: Option<&Execution>,
76 adapter: &BoxTargetAdapter,
77 python_version: &str,
78 files: &BTreeSet<String>,
79) -> Result<()> {
80 let Some(execution) = execution else {
81 return Ok(());
82 };
83 match execution {
84 Execution::PythonScript { script, .. } => {
85 let safe = safe_relative_path(script)?;
86 if !files.contains(&safe) {
87 fail!("Execution script is missing from the box: {safe}.");
88 }
89 }
90 Execution::PythonModule { module, .. } => {
91 let candidates = module_entry_points(adapter, module, python_version)?;
92 if !candidates.iter().any(|path| files.contains(path)) {
93 fail!("Execution module is not discoverable in the box: {module}.");
94 }
95 }
96 }
97 Ok(())
98}
99
100#[cfg(test)]
101mod tests {
102 use super::{assert_execution_files, python_major_minor};
103 use crate::contract::targets::{box_target_adapter, BoxTarget};
104 use crate::release::Execution;
105 use std::collections::BTreeSet;
106
107 fn adapter(platform: &str, arch: &str, accelerator: &str) -> &'static crate::contract::targets::BoxTargetAdapter {
108 box_target_adapter(&BoxTarget {
109 platform: platform.to_string(),
110 arch: arch.to_string(),
111 accelerator: accelerator.to_string(),
112 cuda_version: None,
113 })
114 .unwrap()
115 }
116
117 fn files(paths: &[&str]) -> BTreeSet<String> {
118 paths.iter().map(|path| (*path).to_string()).collect()
119 }
120
121 #[test]
122 fn a_script_must_exist_as_a_regular_entry() {
123 let adapter = adapter("linux", "x86_64", "cpu");
124 let execution = Execution::PythonScript {
125 script: "app/main.py".to_string(),
126 default_args: vec![],
127 };
128 assert!(
129 assert_execution_files(Some(&execution), adapter, "3.11.9", &files(&["app/main.py"]))
130 .is_ok()
131 );
132 let error =
133 assert_execution_files(Some(&execution), adapter, "3.11.9", &files(&["app/other.py"]))
134 .unwrap_err();
135 assert!(error.message().contains("Execution script is missing"), "{error}");
136 }
137
138 #[test]
139 fn a_module_resolves_through_any_of_its_legitimate_locations() {
140 let adapter = adapter("linux", "x86_64", "cpu");
141 let execution = Execution::PythonModule {
142 module: "example_model.main".to_string(),
143 default_args: vec![],
144 };
145 for location in [
146 "example_model/main.py",
147 "example_model/main/__main__.py",
148 "venv/lib/python3.11/example_model/main.py",
149 "venv/lib/python3.11/site-packages/example_model/main.py",
150 "venv/lib/python3.11/site-packages/example_model/main/__main__.py",
151 ] {
152 assert!(
153 assert_execution_files(Some(&execution), adapter, "3.11.9", &files(&[location]))
154 .is_ok(),
155 "{location} did not resolve"
156 );
157 }
158 let error =
159 assert_execution_files(Some(&execution), adapter, "3.11.9", &files(&["elsewhere.py"]))
160 .unwrap_err();
161 assert!(
162 error.message().contains("Execution module is not discoverable"),
163 "{error}"
164 );
165 }
166
167 #[test]
168 fn windows_looks_in_its_own_standard_library() {
169 let windows = adapter("windows", "x86_64", "cpu");
170 let execution = Execution::PythonModule {
171 module: "pkg".to_string(),
172 default_args: vec![],
173 };
174 assert!(assert_execution_files(
175 Some(&execution),
176 windows,
177 "3.11.9",
178 &files(&["venv/Lib/site-packages/pkg/__main__.py"])
179 )
180 .is_ok());
181 assert!(assert_execution_files(
183 Some(&execution),
184 windows,
185 "3.11.9",
186 &files(&["venv/lib/python3.11/site-packages/pkg/__main__.py"])
187 )
188 .is_err());
189 }
190
191 #[test]
192 fn a_python_version_that_cannot_locate_a_standard_library_is_refused() {
193 assert_eq!(python_major_minor("3.11.9").unwrap(), "3.11");
194 assert_eq!(python_major_minor("3.12").unwrap(), "3.12");
195 for invalid in ["", "3", "3.x", "x.1", "3."] {
196 assert!(python_major_minor(invalid).is_err(), "{invalid} was accepted");
197 }
198 }
199
200 #[test]
201 fn a_library_only_box_declares_no_execution() {
202 let adapter = adapter("macos", "aarch64", "metal");
203 assert!(assert_execution_files(None, adapter, "3.11.9", &files(&[])).is_ok());
204 }
205}