1 // This is a script to deploy and execute a binary on an iOS simulator.
2 // The primary use of this is to be able to run unit tests on the simulator and
3 // retrieve the results.
4 //
5 // To do this through Cargo instead, use Dinghy
6 // (https://github.com/snipsco/dinghy): cargo dinghy install, then cargo dinghy
7 // test.
8 
9 use std::env;
10 use std::fs::{self, File};
11 use std::io::Write;
12 use std::path::Path;
13 use std::process;
14 use std::process::Command;
15 
16 macro_rules! t {
17     ($e:expr) => (match $e {
18         Ok(e) => e,
19         Err(e) => panic!("{} failed with: {e}", stringify!($e)),
20     })
21 }
22 
23 // Step one: Wrap as an app
package_as_simulator_app(crate_name: &str, test_binary_path: &Path)24 fn package_as_simulator_app(crate_name: &str, test_binary_path: &Path) {
25     println!("Packaging simulator app");
26     drop(fs::remove_dir_all("ios_simulator_app"));
27     t!(fs::create_dir("ios_simulator_app"));
28     t!(fs::copy(test_binary_path,
29                 Path::new("ios_simulator_app").join(crate_name)));
30 
31     let mut f = t!(File::create("ios_simulator_app/Info.plist"));
32     t!(f.write_all(format!(r#"
33         <?xml version="1.0" encoding="UTF-8"?>
34         <!DOCTYPE plist PUBLIC
35                 "-//Apple//DTD PLIST 1.0//EN"
36                 "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
37         <plist version="1.0">
38             <dict>
39                 <key>CFBundleExecutable</key>
40                 <string>{}</string>
41                 <key>CFBundleIdentifier</key>
42                 <string>com.rust.unittests</string>
43             </dict>
44         </plist>
45     "#, crate_name).as_bytes()));
46 }
47 
48 // Step two: Start the iOS simulator
start_simulator()49 fn start_simulator() {
50     println!("Looking for iOS simulator");
51     let output = t!(Command::new("xcrun").arg("simctl").arg("list").output());
52     assert!(output.status.success());
53     let mut simulator_exists = false;
54     let mut simulator_booted = false;
55     let mut found_rust_sim = false;
56     let stdout = t!(String::from_utf8(output.stdout));
57     for line in stdout.lines() {
58         if line.contains("rust_ios") {
59             if found_rust_sim {
60                 panic!("Duplicate rust_ios simulators found. Please \
61                         double-check xcrun simctl list.");
62             }
63             simulator_exists = true;
64             simulator_booted = line.contains("(Booted)");
65             found_rust_sim = true;
66         }
67     }
68 
69     if simulator_exists == false {
70         println!("Creating iOS simulator");
71         Command::new("xcrun")
72                 .arg("simctl")
73                 .arg("create")
74                 .arg("rust_ios")
75                 .arg("com.apple.CoreSimulator.SimDeviceType.iPhone-SE")
76                 .arg("com.apple.CoreSimulator.SimRuntime.iOS-10-2")
77                 .check_status();
78     } else if simulator_booted == true {
79         println!("Shutting down already-booted simulator");
80         Command::new("xcrun")
81                 .arg("simctl")
82                 .arg("shutdown")
83                 .arg("rust_ios")
84                 .check_status();
85     }
86 
87     println!("Starting iOS simulator");
88     // We can't uninstall the app (if present) as that will hang if the
89     // simulator isn't completely booted; just erase the simulator instead.
90     Command::new("xcrun").arg("simctl").arg("erase").arg("rust_ios").check_status();
91     Command::new("xcrun").arg("simctl").arg("boot").arg("rust_ios").check_status();
92 }
93 
94 // Step three: Install the app
install_app_to_simulator()95 fn install_app_to_simulator() {
96     println!("Installing app to simulator");
97     Command::new("xcrun")
98             .arg("simctl")
99             .arg("install")
100             .arg("booted")
101             .arg("ios_simulator_app/")
102             .check_status();
103 }
104 
105 // Step four: Run the app
run_app_on_simulator()106 fn run_app_on_simulator() {
107     println!("Running app");
108     let output = t!(Command::new("xcrun")
109                     .arg("simctl")
110                     .arg("launch")
111                     .arg("--console")
112                     .arg("booted")
113                     .arg("com.rust.unittests")
114                     .output());
115 
116     println!("status: {}", output.status);
117     println!("stdout --\n{}\n", String::from_utf8_lossy(&output.stdout));
118     println!("stderr --\n{}\n", String::from_utf8_lossy(&output.stderr));
119 
120     let stdout = String::from_utf8_lossy(&output.stdout);
121     let passed = stdout.lines()
122                        .find(|l|
123                              (l.contains("PASSED") &&
124                               l.contains("tests")) ||
125                              l.contains("test result: ok")
126                         )
127                        .unwrap_or(false);
128 
129     println!("Shutting down simulator");
130     Command::new("xcrun")
131         .arg("simctl")
132         .arg("shutdown")
133         .arg("rust_ios")
134         .check_status();
135     if !passed {
136         panic!("tests didn't pass");
137     }
138 }
139 
140 trait CheckStatus {
check_status(&mut self)141     fn check_status(&mut self);
142 }
143 
144 impl CheckStatus for Command {
check_status(&mut self)145     fn check_status(&mut self) {
146         println!("\trunning: {self:?}");
147         assert!(t!(self.status()).success());
148     }
149 }
150 
main()151 fn main() {
152     let args: Vec<String> = env::args().collect();
153     if args.len() != 2 {
154         println!("Usage: {} <executable>", args[0]);
155         process::exit(-1);
156     }
157 
158     let test_binary_path = Path::new(&args[1]);
159     let crate_name = test_binary_path.file_name().unwrap();
160 
161     package_as_simulator_app(crate_name.to_str().unwrap(), test_binary_path);
162     start_simulator();
163     install_app_to_simulator();
164     run_app_on_simulator();
165 }
166