Skip to main content

ios_core/services/testmanager/
xctestrun.rs

1use 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}