ios-core 0.1.7

High-level device API, pairing transport, and discovery for iOS devices
Documentation
use std::collections::HashMap;
use std::path::Path;

use plist::Value;
use serde::Deserialize;

#[derive(Debug, thiserror::Error)]
pub enum XctestRunError {
    #[error("io error: {0}")]
    Io(#[from] std::io::Error),
    #[error("plist parse error: {0}")]
    Plist(#[from] plist::Error),
    #[error("missing __xctestrun_metadata__.FormatVersion")]
    MissingFormatVersion,
    #[error("unsupported .xctestrun format version {0}")]
    UnsupportedFormatVersion(i64),
    #[error("the provided .xctestrun file does not contain any test configurations")]
    EmptyConfigurations,
}

#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct SchemeData {
    #[serde(rename = "TestHostBundleIdentifier", default)]
    pub test_host_bundle_identifier: String,
    #[serde(rename = "TestBundlePath", default)]
    pub test_bundle_path: String,
    #[serde(rename = "SkipTestIdentifiers", default)]
    pub skip_test_identifiers: Vec<String>,
    #[serde(rename = "OnlyTestIdentifiers", default)]
    pub only_test_identifiers: Vec<String>,
    #[serde(rename = "IsUITestBundle", default)]
    pub is_ui_test_bundle: bool,
    #[serde(rename = "CommandLineArguments", default)]
    pub command_line_arguments: Vec<String>,
    #[serde(rename = "EnvironmentVariables", default)]
    pub environment_variables: HashMap<String, Value>,
    #[serde(rename = "TestingEnvironmentVariables", default)]
    pub testing_environment_variables: HashMap<String, Value>,
    #[serde(rename = "UITargetAppEnvironmentVariables", default)]
    pub ui_target_app_environment_variables: HashMap<String, Value>,
    #[serde(rename = "UITargetAppCommandLineArguments", default)]
    pub ui_target_app_command_line_arguments: Vec<String>,
    #[serde(rename = "UITargetAppPath", default)]
    pub ui_target_app_path: String,
}

#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct TestConfiguration {
    #[serde(rename = "Name", default)]
    pub name: String,
    #[serde(rename = "TestTargets", default)]
    pub test_targets: Vec<SchemeData>,
}

pub fn parse_xctestrun_file(
    path: impl AsRef<Path>,
) -> Result<Vec<TestConfiguration>, XctestRunError> {
    let bytes = std::fs::read(path)?;
    parse_xctestrun_bytes(&bytes)
}

pub fn parse_xctestrun_bytes(bytes: &[u8]) -> Result<Vec<TestConfiguration>, XctestRunError> {
    let root = Value::from_reader_xml(bytes)
        .or_else(|_| Value::from_reader(std::io::Cursor::new(bytes)))?;
    let version = format_version(&root)?;
    match version {
        1 => parse_version_1(root),
        2 => parse_version_2(root),
        other => Err(XctestRunError::UnsupportedFormatVersion(other)),
    }
}

fn format_version(root: &Value) -> Result<i64, XctestRunError> {
    let dict = root
        .as_dictionary()
        .ok_or(XctestRunError::MissingFormatVersion)?;
    dict.get("__xctestrun_metadata__")
        .and_then(Value::as_dictionary)
        .and_then(|metadata| metadata.get("FormatVersion"))
        .and_then(Value::as_signed_integer)
        .ok_or(XctestRunError::MissingFormatVersion)
}

fn parse_version_1(root: Value) -> Result<Vec<TestConfiguration>, XctestRunError> {
    let dict = root
        .into_dictionary()
        .ok_or(XctestRunError::MissingFormatVersion)?;
    for (key, value) in dict {
        if key == "__xctestrun_metadata__" {
            continue;
        }

        let scheme: SchemeData = plist::from_value(&value)?;
        return Ok(vec![TestConfiguration {
            name: String::new(),
            test_targets: vec![scheme],
        }]);
    }

    Err(XctestRunError::EmptyConfigurations)
}

fn parse_version_2(root: Value) -> Result<Vec<TestConfiguration>, XctestRunError> {
    #[derive(Deserialize)]
    struct Version2Root {
        #[serde(rename = "TestConfigurations", default)]
        test_configurations: Vec<TestConfiguration>,
    }

    let parsed: Version2Root = plist::from_value(&root)?;
    if parsed.test_configurations.is_empty() {
        return Err(XctestRunError::EmptyConfigurations);
    }
    Ok(parsed.test_configurations)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parses_version_1_xctestrun() {
        let plist = br#"<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>DemoApp</key>
  <dict>
    <key>TestHostBundleIdentifier</key><string>com.example.DemoAppUITests.xctrunner</string>
    <key>TestBundlePath</key><string>DemoAppUITests.xctest</string>
    <key>IsUITestBundle</key><true/>
    <key>CommandLineArguments</key><array><string>-ApplePersistenceIgnoreState</string></array>
  </dict>
  <key>__xctestrun_metadata__</key>
  <dict><key>FormatVersion</key><integer>1</integer></dict>
</dict>
</plist>"#;

        let configs = parse_xctestrun_bytes(plist).unwrap();
        assert_eq!(configs.len(), 1);
        assert_eq!(
            configs[0].test_targets[0].test_host_bundle_identifier,
            "com.example.DemoAppUITests.xctrunner"
        );
        assert!(configs[0].test_targets[0].is_ui_test_bundle);
    }

    #[test]
    fn parses_version_2_xctestrun() {
        let plist = br#"<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>TestConfigurations</key>
  <array>
    <dict>
      <key>Name</key><string>UITests</string>
      <key>TestTargets</key>
      <array>
        <dict>
          <key>TestHostBundleIdentifier</key><string>com.example.DemoAppUITests.xctrunner</string>
          <key>TestBundlePath</key><string>DemoAppUITests.xctest</string>
          <key>IsUITestBundle</key><true/>
        </dict>
      </array>
    </dict>
  </array>
  <key>__xctestrun_metadata__</key>
  <dict><key>FormatVersion</key><integer>2</integer></dict>
</dict>
</plist>"#;

        let configs = parse_xctestrun_bytes(plist).unwrap();
        assert_eq!(configs.len(), 1);
        assert_eq!(configs[0].name, "UITests");
        assert_eq!(
            configs[0].test_targets[0].test_bundle_path,
            "DemoAppUITests.xctest"
        );
    }

    #[test]
    fn parses_ui_target_app_command_line_arguments() {
        let plist = br#"<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>DemoApp</key>
  <dict>
    <key>TestHostBundleIdentifier</key><string>com.example.DemoAppUITests.xctrunner</string>
    <key>TestBundlePath</key><string>DemoAppUITests.xctest</string>
    <key>IsUITestBundle</key><true/>
    <key>UITargetAppCommandLineArguments</key>
    <array>
      <string>-AppleLanguages</string>
      <string>(en)</string>
    </array>
  </dict>
  <key>__xctestrun_metadata__</key>
  <dict><key>FormatVersion</key><integer>1</integer></dict>
</dict>
</plist>"#;

        let configs = parse_xctestrun_bytes(plist).unwrap();
        assert_eq!(
            configs[0].test_targets[0].ui_target_app_command_line_arguments,
            vec!["-AppleLanguages".to_string(), "(en)".to_string()]
        );
    }
}