ios_core/services/testmanager/
xctestrun.rs1use std::collections::HashMap;
2use std::path::Path;
3
4use plist::Value;
5use serde::Deserialize;
6
7#[derive(Debug, thiserror::Error)]
8pub enum XctestRunError {
9 #[error("io error: {0}")]
10 Io(#[from] std::io::Error),
11 #[error("plist parse error: {0}")]
12 Plist(#[from] plist::Error),
13 #[error("missing __xctestrun_metadata__.FormatVersion")]
14 MissingFormatVersion,
15 #[error("unsupported .xctestrun format version {0}")]
16 UnsupportedFormatVersion(i64),
17 #[error("the provided .xctestrun file does not contain any test configurations")]
18 EmptyConfigurations,
19}
20
21#[derive(Debug, Clone, PartialEq, Deserialize)]
22pub struct SchemeData {
23 #[serde(rename = "TestHostBundleIdentifier", default)]
24 pub test_host_bundle_identifier: String,
25 #[serde(rename = "TestBundlePath", default)]
26 pub test_bundle_path: String,
27 #[serde(rename = "SkipTestIdentifiers", default)]
28 pub skip_test_identifiers: Vec<String>,
29 #[serde(rename = "OnlyTestIdentifiers", default)]
30 pub only_test_identifiers: Vec<String>,
31 #[serde(rename = "IsUITestBundle", default)]
32 pub is_ui_test_bundle: bool,
33 #[serde(rename = "CommandLineArguments", default)]
34 pub command_line_arguments: Vec<String>,
35 #[serde(rename = "EnvironmentVariables", default)]
36 pub environment_variables: HashMap<String, Value>,
37 #[serde(rename = "TestingEnvironmentVariables", default)]
38 pub testing_environment_variables: HashMap<String, Value>,
39 #[serde(rename = "UITargetAppEnvironmentVariables", default)]
40 pub ui_target_app_environment_variables: HashMap<String, Value>,
41 #[serde(rename = "UITargetAppCommandLineArguments", default)]
42 pub ui_target_app_command_line_arguments: Vec<String>,
43 #[serde(rename = "UITargetAppPath", default)]
44 pub ui_target_app_path: String,
45}
46
47#[derive(Debug, Clone, PartialEq, Deserialize)]
48pub struct TestConfiguration {
49 #[serde(rename = "Name", default)]
50 pub name: String,
51 #[serde(rename = "TestTargets", default)]
52 pub test_targets: Vec<SchemeData>,
53}
54
55pub fn parse_xctestrun_file(
56 path: impl AsRef<Path>,
57) -> Result<Vec<TestConfiguration>, XctestRunError> {
58 let bytes = std::fs::read(path)?;
59 parse_xctestrun_bytes(&bytes)
60}
61
62pub fn parse_xctestrun_bytes(bytes: &[u8]) -> Result<Vec<TestConfiguration>, XctestRunError> {
63 let root = Value::from_reader_xml(bytes)
64 .or_else(|_| Value::from_reader(std::io::Cursor::new(bytes)))?;
65 let version = format_version(&root)?;
66 match version {
67 1 => parse_version_1(root),
68 2 => parse_version_2(root),
69 other => Err(XctestRunError::UnsupportedFormatVersion(other)),
70 }
71}
72
73fn format_version(root: &Value) -> Result<i64, XctestRunError> {
74 let dict = root
75 .as_dictionary()
76 .ok_or(XctestRunError::MissingFormatVersion)?;
77 dict.get("__xctestrun_metadata__")
78 .and_then(Value::as_dictionary)
79 .and_then(|metadata| metadata.get("FormatVersion"))
80 .and_then(Value::as_signed_integer)
81 .ok_or(XctestRunError::MissingFormatVersion)
82}
83
84fn parse_version_1(root: Value) -> Result<Vec<TestConfiguration>, XctestRunError> {
85 let dict = root
86 .into_dictionary()
87 .ok_or(XctestRunError::MissingFormatVersion)?;
88 for (key, value) in dict {
89 if key == "__xctestrun_metadata__" {
90 continue;
91 }
92
93 let scheme: SchemeData = plist::from_value(&value)?;
94 return Ok(vec![TestConfiguration {
95 name: String::new(),
96 test_targets: vec![scheme],
97 }]);
98 }
99
100 Err(XctestRunError::EmptyConfigurations)
101}
102
103fn parse_version_2(root: Value) -> Result<Vec<TestConfiguration>, XctestRunError> {
104 #[derive(Deserialize)]
105 struct Version2Root {
106 #[serde(rename = "TestConfigurations", default)]
107 test_configurations: Vec<TestConfiguration>,
108 }
109
110 let parsed: Version2Root = plist::from_value(&root)?;
111 if parsed.test_configurations.is_empty() {
112 return Err(XctestRunError::EmptyConfigurations);
113 }
114 Ok(parsed.test_configurations)
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 #[test]
122 fn parses_version_1_xctestrun() {
123 let plist = br#"<?xml version="1.0" encoding="UTF-8"?>
124<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
125<plist version="1.0">
126<dict>
127 <key>DemoApp</key>
128 <dict>
129 <key>TestHostBundleIdentifier</key><string>com.example.DemoAppUITests.xctrunner</string>
130 <key>TestBundlePath</key><string>DemoAppUITests.xctest</string>
131 <key>IsUITestBundle</key><true/>
132 <key>CommandLineArguments</key><array><string>-ApplePersistenceIgnoreState</string></array>
133 </dict>
134 <key>__xctestrun_metadata__</key>
135 <dict><key>FormatVersion</key><integer>1</integer></dict>
136</dict>
137</plist>"#;
138
139 let configs = parse_xctestrun_bytes(plist).unwrap();
140 assert_eq!(configs.len(), 1);
141 assert_eq!(
142 configs[0].test_targets[0].test_host_bundle_identifier,
143 "com.example.DemoAppUITests.xctrunner"
144 );
145 assert!(configs[0].test_targets[0].is_ui_test_bundle);
146 }
147
148 #[test]
149 fn parses_version_2_xctestrun() {
150 let plist = br#"<?xml version="1.0" encoding="UTF-8"?>
151<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
152<plist version="1.0">
153<dict>
154 <key>TestConfigurations</key>
155 <array>
156 <dict>
157 <key>Name</key><string>UITests</string>
158 <key>TestTargets</key>
159 <array>
160 <dict>
161 <key>TestHostBundleIdentifier</key><string>com.example.DemoAppUITests.xctrunner</string>
162 <key>TestBundlePath</key><string>DemoAppUITests.xctest</string>
163 <key>IsUITestBundle</key><true/>
164 </dict>
165 </array>
166 </dict>
167 </array>
168 <key>__xctestrun_metadata__</key>
169 <dict><key>FormatVersion</key><integer>2</integer></dict>
170</dict>
171</plist>"#;
172
173 let configs = parse_xctestrun_bytes(plist).unwrap();
174 assert_eq!(configs.len(), 1);
175 assert_eq!(configs[0].name, "UITests");
176 assert_eq!(
177 configs[0].test_targets[0].test_bundle_path,
178 "DemoAppUITests.xctest"
179 );
180 }
181
182 #[test]
183 fn parses_ui_target_app_command_line_arguments() {
184 let plist = br#"<?xml version="1.0" encoding="UTF-8"?>
185<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
186<plist version="1.0">
187<dict>
188 <key>DemoApp</key>
189 <dict>
190 <key>TestHostBundleIdentifier</key><string>com.example.DemoAppUITests.xctrunner</string>
191 <key>TestBundlePath</key><string>DemoAppUITests.xctest</string>
192 <key>IsUITestBundle</key><true/>
193 <key>UITargetAppCommandLineArguments</key>
194 <array>
195 <string>-AppleLanguages</string>
196 <string>(en)</string>
197 </array>
198 </dict>
199 <key>__xctestrun_metadata__</key>
200 <dict><key>FormatVersion</key><integer>1</integer></dict>
201</dict>
202</plist>"#;
203
204 let configs = parse_xctestrun_bytes(plist).unwrap();
205 assert_eq!(
206 configs[0].test_targets[0].ui_target_app_command_line_arguments,
207 vec!["-AppleLanguages".to_string(), "(en)".to_string()]
208 );
209 }
210}