use std::path::Path;
use crate::runner::RunnerState;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Platform {
Ios,
Android,
}
impl Platform {
fn key(self) -> &'static str {
match self {
Platform::Ios => "runner-ios",
Platform::Android => "runner-android",
}
}
}
fn store_root(root: &Path) -> std::path::PathBuf {
root.join(".smix")
}
fn open(root: &Path) -> Result<smix_store::Store, String> {
smix_store::Store::open(&store_root(root))
.map_err(|e| format!("open runner store under {}: {e}", root.display()))
}
pub fn read(root: &Path, platform: Platform) -> Result<Option<RunnerState>, String> {
let store = open(root)?;
if let Some(state) = store
.singleton(platform.key())
.get_json::<RunnerState>()
.map_err(|e| format!("read runner state: {e}"))?
{
return Ok(Some(state));
}
read_legacy(root)
}
fn read_legacy(root: &Path) -> Result<Option<RunnerState>, String> {
let legacy = root.join(".smix/runner/state.json");
let text = match std::fs::read_to_string(&legacy) {
Ok(t) => t,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(e) => return Err(format!("read {}: {e}", legacy.display())),
};
serde_json::from_str(&text)
.map(Some)
.map_err(|e| format!("{} is not a runner state: {e}", legacy.display()))
}
pub fn write(root: &Path, platform: Platform, state: &RunnerState) -> Result<(), String> {
let store = open(root)?;
store
.singleton(platform.key())
.put_json(state)
.map_err(|e| format!("write runner state: {e}"))?;
store
.sync()
.map_err(|e| format!("persist runner state: {e}"))
}
pub fn clear(root: &Path, platform: Platform) -> Result<(), String> {
let store = open(root)?;
store
.singleton(platform.key())
.delete()
.map_err(|e| format!("clear runner state: {e}"))?;
store
.sync()
.map_err(|e| format!("persist runner state: {e}"))
}
#[cfg(test)]
mod tests {
use super::*;
fn temp_root(name: &str) -> std::path::PathBuf {
let dir = std::env::temp_dir().join(format!("smix-runner-state-{name}"));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("temp root");
dir
}
fn state(udid: &str, port: u16) -> RunnerState {
RunnerState {
pid: 4242,
udid: udid.to_string(),
port,
log: std::path::PathBuf::from("/tmp/runner.log"),
bundle: Some("com.example.app".to_string()),
supervisor_pid: None,
}
}
#[test]
fn state_round_trips_through_the_store() {
let root = temp_root("roundtrip");
write(&root, Platform::Ios, &state("UDID-1", 22087)).expect("write");
let back = read(&root, Platform::Ios).expect("read").expect("present");
assert_eq!(back.udid, "UDID-1");
assert_eq!(back.port, 22087);
}
#[test]
fn the_legacy_state_file_is_not_written() {
let root = temp_root("no-file");
write(&root, Platform::Ios, &state("UDID-1", 22087)).expect("write");
assert!(
!root.join(".smix/runner/state.json").exists(),
"the legacy state.json is still being written"
);
}
#[test]
fn ios_and_android_do_not_overwrite_each_other() {
let root = temp_root("two-platforms");
write(&root, Platform::Ios, &state("IOS-UDID", 22087)).expect("write ios");
write(&root, Platform::Android, &state("emulator-5554", 28080)).expect("write android");
let ios = read(&root, Platform::Ios).expect("read").expect("present");
let android = read(&root, Platform::Android)
.expect("read")
.expect("present");
assert_eq!(
ios.udid, "IOS-UDID",
"the Android runner replaced the iOS record"
);
assert_eq!(android.udid, "emulator-5554");
}
#[test]
fn an_absent_state_is_none_not_an_error() {
let root = temp_root("absent");
assert!(read(&root, Platform::Ios).expect("no error").is_none());
}
#[test]
fn clearing_removes_only_that_platform() {
let root = temp_root("clear");
write(&root, Platform::Ios, &state("IOS", 22087)).expect("write");
write(&root, Platform::Android, &state("droid", 28080)).expect("write");
clear(&root, Platform::Ios).expect("clear");
assert!(read(&root, Platform::Ios).expect("read").is_none());
assert!(
read(&root, Platform::Android).expect("read").is_some(),
"clearing iOS took Android with it"
);
}
#[test]
fn a_pre_store_workspace_still_finds_its_runner() {
let root = temp_root("legacy");
let dir = root.join(".smix/runner");
std::fs::create_dir_all(&dir).expect("dirs");
std::fs::write(
dir.join("state.json"),
serde_json::to_string(&state("LEGACY-UDID", 22087)).expect("json"),
)
.expect("write legacy");
let back = read(&root, Platform::Ios).expect("read").expect("present");
assert_eq!(back.udid, "LEGACY-UDID");
assert!(
root.join(".smix/runner/state.json").exists(),
"the legacy file must be left where it is"
);
}
#[test]
fn a_corrupt_stored_state_is_named_not_silently_absent() {
let root = temp_root("corrupt");
{
let store = smix_store::Store::open(&root.join(".smix")).expect("open");
store
.singleton("runner-ios")
.put_json(&"not a runner state")
.expect("put");
}
let err = read(&root, Platform::Ios).expect_err("must not read as absent");
assert!(
err.contains("runner-ios"),
"the error must name what it could not read: {err}"
);
}
}