diff --git a/Cargo.lock b/Cargo.lock index d490cf8..76b4e4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,54 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "anstream" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" + +[[package]] +name = "anstyle-parse" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +dependencies = [ + "anstyle", + "windows-sys", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -41,6 +89,18 @@ dependencies = [ [[package]] name = "cargo-finestra" version = "0.3.0" +dependencies = [ + "clap", +] + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] [[package]] name = "cfg-if" @@ -48,6 +108,52 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "clap" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80c21025abd42669a92efc996ef13cfb2c5c627858421ea58d5c3b331a6c134f" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "458bf1f341769dfcf849846f65dffdf9146daa56bcd2a47cb4e1de9915567c99" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "307bc0538d5f0f83b8248db3087aa92fe504e4691294d0c96c0eabc33f47ba47" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + [[package]] name = "core-foundation" version = "0.9.4" @@ -124,6 +230,7 @@ dependencies = [ "cacao", "dashmap", "euclid", + "objc_exception", "objc_id", "windows", ] @@ -158,6 +265,12 @@ version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "idna" version = "0.5.0" @@ -223,6 +336,15 @@ dependencies = [ "malloc_buf", ] +[[package]] +name = "objc_exception" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" +dependencies = [ + "cc", +] + [[package]] name = "objc_id" version = "0.1.1" @@ -334,6 +456,12 @@ version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" +[[package]] +name = "strsim" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" + [[package]] name = "syn" version = "2.0.48" @@ -392,6 +520,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "winapi" version = "0.3.9" @@ -433,6 +567,15 @@ dependencies = [ "windows-targets 0.52.0", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", +] + [[package]] name = "windows-targets" version = "0.48.5" diff --git a/cargo-finestra/Cargo.toml b/cargo-finestra/Cargo.toml index 563a9f6..382cb30 100644 --- a/cargo-finestra/Cargo.toml +++ b/cargo-finestra/Cargo.toml @@ -8,3 +8,4 @@ license.workspace = true description.workspace = true [dependencies] +clap = { version = "4.5.0", features = ["derive"] } diff --git a/finestra/Cargo.toml b/finestra/Cargo.toml index c4569c0..e1772fe 100644 --- a/finestra/Cargo.toml +++ b/finestra/Cargo.toml @@ -16,6 +16,7 @@ euclid = "0.22" cacao = { version = "0.3", features = ["appkit"] } objc_id = "0.1.1" block = "0.1.6" +objc_exception = "0.1.2" [target.'cfg(target_os = "windows")'.dependencies] windows = { version = "0.52", features = [ diff --git a/finestra/build.rs b/finestra/build.rs new file mode 100644 index 0000000..a950460 --- /dev/null +++ b/finestra/build.rs @@ -0,0 +1,15 @@ +// Copyright (C) 2024 Tristan Gerritsen +// All Rights Reserved. + +fn main() { + #[cfg(all(target_os = "macos", test))] + configure_xctest(); +} + +#[cfg(all(target_os = "macos", test))] +fn configure_xctest() { + println!("cargo:rustc-link-lib=framework=XCTest"); + println!("cargo:rustc-link-search=framework=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/Frameworks/"); + + println!("cargo:rustc-env=DYLD_FRAMEWORK_PATH=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/Frameworks/"); +} diff --git a/finestra/src/lib.rs b/finestra/src/lib.rs index 9c58c59..150845f 100644 --- a/finestra/src/lib.rs +++ b/finestra/src/lib.rs @@ -20,6 +20,9 @@ mod state; mod views; mod window; +#[cfg(test)] +pub mod test; + pub use self::app::*; pub(crate) use self::layout::*; pub use self::property::*; diff --git a/finestra/src/platform/macos/mod.rs b/finestra/src/platform/macos/mod.rs index 9529caa..747db85 100644 --- a/finestra/src/platform/macos/mod.rs +++ b/finestra/src/platform/macos/mod.rs @@ -9,6 +9,7 @@ mod extensions; mod resources; pub(crate) mod state; mod window; +mod xctest; pub(crate) use self::app::MacOSDelegate; pub(crate) use self::appkit::*; @@ -18,6 +19,9 @@ pub(crate) use self::resources::ToCacao; pub(crate) use self::dynamic_wrapper::DynamicViewWrapper; pub(crate) use self::dynamic_wrapper::LayoutExt; +#[cfg(test)] +pub(crate) use self::xctest::*; + use cacao::appkit::App as CacaoApp; use crate::{App, AppDelegate}; diff --git a/finestra/src/platform/macos/xctest/mod.rs b/finestra/src/platform/macos/xctest/mod.rs new file mode 100644 index 0000000..6bf8942 --- /dev/null +++ b/finestra/src/platform/macos/xctest/mod.rs @@ -0,0 +1,60 @@ +// Copyright (C) 2024 Tristan Gerritsen +// All Rights Reserved. + +mod xcuiapplication; +mod xcuiapplicationstate; + +use std::{io::{stdout, Write}, sync::Once}; + +use cacao::{foundation::NSString, objc::runtime::Class}; + +pub(crate) use self::{ + xcuiapplication::XCUIApplication, + xcuiapplicationstate::XCUIApplicationState, +}; + +const BUNDLE_LOAD: Once = Once::new(); + +pub(crate) fn load_xc_test_into_bundle() { + println!("LOADBUNDLE"); + + use cacao::objc::{class, msg_send, sel, sel_impl}; + use cacao::objc::runtime::Object; + BUNDLE_LOAD.call_once(|| { + let result = unsafe { + objc_exception::r#try(|| { + let path = NSString::new("/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/Frameworks/XCTest.framework"); + let xctest_framework: *const Object = msg_send![class!(NSBundle), bundleWithPath: &*path.objc]; + let _: () = msg_send![xctest_framework, load]; + }) + }; + + if let Err(e) = result { + panic!("Failed to load bundle: {e:p}"); + } + }); + + for class in Class::classes().into_iter() { + if !class.name().starts_with("XCTestConfiguration") { continue } + eprintln!("\n\nClass: {}", class.name()); + + for protocol in class.adopted_protocols().iter() { + eprintln!(" Protocol: {}", protocol.name()); + } + + for variable in class.instance_variables().iter() { + eprintln!(" Variable: {}", variable.name()); + } + + for method in class.instance_methods().iter() { + eprintln!(" Method: {}", method.name().name()); + eprintln!(" Return Type: {}", method.return_type().as_str()); + eprintln!(" Arguments: {}", method.arguments_count()); + for i in 0..method.arguments_count() { + eprintln!(" * Argument {i}: {:?}", method.argument_type(i).map(|x| x.as_str().to_owned()).unwrap_or_default()); + } + } + } + + println!("done load"); +} diff --git a/finestra/src/platform/macos/xctest/xcuiapplication.rs b/finestra/src/platform/macos/xctest/xcuiapplication.rs new file mode 100644 index 0000000..39db4de --- /dev/null +++ b/finestra/src/platform/macos/xctest/xcuiapplication.rs @@ -0,0 +1,57 @@ +// Copyright (C) 2024 Tristan Gerritsen +// All Rights Reserved. + +use cacao::{foundation::{id, NSString, NSUInteger, NSURL}, objc::{class, msg_send, runtime::Class, sel, sel_impl}, utils::properties::ObjcProperty}; + +use super::XCUIApplicationState; + +pub(crate) struct XCUIApplication { + objc: ObjcProperty, +} + +impl XCUIApplication { + /// + pub fn init_with_url(uurl: &str) -> Self { + super::load_xc_test_into_bundle(); + + let url = NSString::new(uurl); + let obj: id = unsafe { + let url: id = msg_send![class!(NSURL), URLWithString:&*url]; + println!("URL: {url:p} from \"{uurl}\""); + + // let obj: id = msg_send![class!(XCUIApplication), new]; + // let obj: id = msg_send![obj, initWithURL:url]; + let obj: id = msg_send![class!(XCUIApplication), alloc]; + let obj: id = msg_send![obj, initWithURL:url]; + obj + }; + + Self { + objc: ObjcProperty::retain(obj), + } + } + + pub fn activate(&self) { + let _: () = self.objc.get(|objc| unsafe { + msg_send![objc, activate] + }); + } + + pub fn state(&self) -> XCUIApplicationState { + let id: NSUInteger = self.objc.get(|objc| unsafe { + msg_send![ + objc, state + ] + }); + + id.into() + } +} + +impl Drop for XCUIApplication { + fn drop(&mut self) { + self.objc.with_mut(|obj| unsafe { + let _: () = msg_send![obj, terminate]; + }) + } +} diff --git a/finestra/src/platform/macos/xctest/xcuiapplicationstate.rs b/finestra/src/platform/macos/xctest/xcuiapplicationstate.rs new file mode 100644 index 0000000..eaa8936 --- /dev/null +++ b/finestra/src/platform/macos/xctest/xcuiapplicationstate.rs @@ -0,0 +1,26 @@ +// Copyright (C) 2024 Tristan Gerritsen +// All Rights Reserved. + +use cacao::foundation::NSUInteger; + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub(crate) enum XCUIApplicationState { + Unknown, + NotRunning, + RunningBackgroundSuspended, + RunningBackground, + RunningForeground, +} + +impl From for XCUIApplicationState { + fn from(value: NSUInteger) -> Self { + match value { + 0 => Self::Unknown, + 1 => Self::NotRunning, + 2 => Self::RunningBackgroundSuspended, + 3 => Self::RunningBackground, + 4 => Self::RunningForeground, + _ => panic!("Invalid value {value} for XCUIApplicationState"), + } + } +} diff --git a/finestra/src/test/application.rs b/finestra/src/test/application.rs new file mode 100644 index 0000000..6f4402b --- /dev/null +++ b/finestra/src/test/application.rs @@ -0,0 +1,64 @@ +// Copyright (C) 2024 Tristan Gerritsen +// All Rights Reserved. + +use std::path::PathBuf; + +use super::UIProcess; + +/// A wrapper around UI Applications that can be used in test scenarios. +pub struct UITestApplication { + path: PathBuf, + process: UIProcess, +} + +impl UITestApplication { + /// Launch + pub fn launch_by_path(path: impl Into) -> Self { + let path = path.into(); + let process = UIProcess::new(&path); + process.wait_until_ready(); + Self { + path: path, + process, + } + } +} + +#[cfg(test)] +mod tests { + use std::ptr::{null, null_mut}; + + use cacao::{foundation::NSString, objc::{msg_send, runtime::Object, sel, sel_impl}}; + + use super::*; + + #[test] + fn launch_example() { + let test = || { + let app = UITestApplication::launch_by_path("/Users/tager/Developer/Public/Finestra/showcase/showcase.app"); + _ = app; + }; + + if let Err(e) = unsafe { objc_exception::r#try(test) } { + println!("EXCEPTION: {e:p}\n"); + + if std::ptr::null() == e { + return; + } + + unsafe { + let e = e as *mut Object; + + let name = msg_send![e, name]; + if name != null_mut() { + println!(" Name: {}", NSString::retain(name).to_string()); + } + + let reason = msg_send![e, reason]; + if reason != null_mut() { + println!(" Reason: {}", NSString::retain(reason).to_string()); + } + } + } + } +} diff --git a/finestra/src/test/macos.rs b/finestra/src/test/macos.rs new file mode 100644 index 0000000..4697d40 --- /dev/null +++ b/finestra/src/test/macos.rs @@ -0,0 +1,22 @@ +// Copyright (C) 2024 Tristan Gerritsen +// All Rights Reserved. + +use std::path::Path; + +use crate::platform::macos::XCUIApplication; + +pub struct UIProcess { + xc_app: XCUIApplication, +} + +impl UIProcess { + pub fn new(path: &Path) -> Self { + Self { + xc_app: XCUIApplication::init_with_url(path.to_str().unwrap()), + } + } + + pub fn wait_until_ready(&self) { + self.xc_app.activate(); + } +} diff --git a/finestra/src/test/mod.rs b/finestra/src/test/mod.rs new file mode 100644 index 0000000..043ef84 --- /dev/null +++ b/finestra/src/test/mod.rs @@ -0,0 +1,18 @@ +// Copyright (C) 2024 Tristan Gerritsen +// All Rights Reserved. + +//! This module contains various testing facilities to aid User Interface Tests. + +#[cfg(target_os = "macos")] +mod macos; + +#[cfg(target_os = "macos")] +use macos::*; + +#[cfg(not(target_os = "macos"))] +mod stub; + +#[cfg(not(target_os = "macos"))] +use stub::*; + +mod application; diff --git a/finestra/src/test/stub.rs b/finestra/src/test/stub.rs new file mode 100644 index 0000000..7c198ad --- /dev/null +++ b/finestra/src/test/stub.rs @@ -0,0 +1,18 @@ +// Copyright (C) 2024 Tristan Gerritsen +// All Rights Reserved. + +use std::path::Path; + +pub struct UIProcess { + +} + +impl UIProcess { + pub fn new(path: &Path) -> Self { + Self { + + } + } + + pub fn wait_until_ready(&self) {} +}