-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
✨ feat: Add basic test handler for GUI
- Loading branch information
Showing
10 changed files
with
373 additions
and
6 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,216 @@ | ||
//! Executor gor GUI | ||
//! | ||
use std::{any::Any, error::Error, time::Instant}; | ||
|
||
use image::RgbaImage; | ||
|
||
use crate::{ | ||
exec::gui_handler::{ | ||
basic_handle::basic_handle_once, | ||
handler_api::HandlerCollector, | ||
}, | ||
gui::screen::{DynScreen, Screen}, | ||
info, | ||
util::anybase::AnyBase, | ||
}; | ||
|
||
use super::{gui_api::GuiTestApi, needle::Needle}; | ||
|
||
pub struct GuiTestor { | ||
inner: DynScreen, | ||
} | ||
|
||
impl GuiTestor { | ||
pub fn build(inner: DynScreen) -> GuiTestor { | ||
GuiTestor { inner } | ||
} | ||
} | ||
|
||
impl AnyBase for GuiTestor { | ||
fn as_any(&self) -> &dyn Any { | ||
self | ||
} | ||
fn as_any_mut(&mut self) -> &mut dyn Any { | ||
self | ||
} | ||
fn into_any(self: Box<Self>) -> Box<dyn Any> { | ||
self | ||
} | ||
} | ||
|
||
impl Screen for GuiTestor { | ||
fn size(&self) -> (u32, u32) { | ||
self.inner.size() | ||
} | ||
fn read(&mut self) -> Result<RgbaImage, Box<dyn Error>> { | ||
self.inner.read() | ||
} | ||
fn move_to(&mut self, x: u32, y: u32) -> Result<(), Box<dyn Error>> { | ||
self.inner.move_to(x, y) | ||
} | ||
fn click_left(&mut self) -> Result<(), Box<dyn Error>> { | ||
self.inner.click_left() | ||
} | ||
fn click_right(&mut self) -> Result<(), Box<dyn Error>> { | ||
self.inner.click_right() | ||
} | ||
fn click_middle(&mut self) -> Result<(), Box<dyn Error>> { | ||
self.inner.click_middle() | ||
} | ||
fn scroll_up(&mut self, len: u32) -> Result<(), Box<dyn Error>> { | ||
self.inner.scroll_up(len) | ||
} | ||
fn scroll_down(&mut self, len: u32) -> Result<(), Box<dyn Error>> { | ||
self.inner.scroll_down(len) | ||
} | ||
fn write(&mut self, data: String) -> Result<(), Box<dyn Error>> { | ||
self.inner.write(data) | ||
} | ||
fn hold(&mut self, key: u16) -> Result<(), Box<dyn Error>> { | ||
self.inner.hold(key) | ||
} | ||
fn release(&mut self, key: u16) -> Result<(), Box<dyn Error>> { | ||
self.inner.release(key) | ||
} | ||
} | ||
|
||
struct TimeOutErr; | ||
|
||
impl Error for TimeOutErr {} | ||
|
||
impl std::fmt::Debug for TimeOutErr { | ||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
write!(f, "Timeout") | ||
} | ||
} | ||
|
||
impl std::fmt::Display for TimeOutErr { | ||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
write!(f, "Timeout") | ||
} | ||
} | ||
|
||
/// Wait for a timeout and do something | ||
/// | ||
/// func should return `Some(T)` if the operation is done, `None` if the operation is not done yet, and `Err(E)` if an error occurred. | ||
fn wait_timeout_do<T>( | ||
timeout: u32, | ||
func: &mut dyn FnMut() -> Result<Option<T>, Box<dyn Error>>, | ||
) -> Result<T, Box<dyn Error>> { | ||
let begin = Instant::now(); | ||
loop { | ||
match func() { | ||
Ok(Some(v)) => return Ok(v), | ||
Ok(None) => {} | ||
Err(e) => return Err(e), | ||
} | ||
if begin.elapsed().as_secs() >= timeout as u64 { | ||
return Err(TimeOutErr.into()); | ||
} | ||
} | ||
} | ||
|
||
impl GuiTestApi for GuiTestor { | ||
fn assert_screen(&mut self, needle: &Needle, timeout: u32) -> Result<(), Box<dyn Error>> { | ||
info!("Waiting for screen..."); | ||
wait_timeout_do(timeout, &mut || { | ||
let screen = self.read()?; | ||
let mut res = None; | ||
for handler in inventory::iter::<HandlerCollector> { | ||
if !handler.inner.can_handle(&needle) { | ||
continue; | ||
} | ||
res = Some(handler.inner.handle(&needle, &screen) || res.unwrap_or(false)); | ||
} | ||
match res { | ||
Some(true) => Ok(Some(())), | ||
Some(false) => Ok(None), | ||
None => Err("No handler found".into()), | ||
} | ||
}) | ||
} | ||
fn assert_screen_click(&mut self, needle: &Needle, timeout: u32) -> Result<(), Box<dyn Error>> { | ||
self.assert_screen(&needle, timeout)?; | ||
if !needle.is_basic() { | ||
return Err("Only Basic Needle support this".into()); | ||
} | ||
let areas = match needle { | ||
Needle::Basic(areas) => areas, | ||
_ => unreachable!(), | ||
}; | ||
let screen = self.read()?; | ||
for area in areas { | ||
if area.click_point.is_none() { | ||
continue; | ||
} | ||
if !basic_handle_once(area, &screen) { | ||
continue; | ||
} | ||
let point = area.click_point.as_ref().unwrap(); | ||
self.click_left_at(point.xpos, point.ypos)?; | ||
} | ||
Ok(()) | ||
} | ||
fn wait_screen_change( | ||
&mut self, | ||
timeout: u32, | ||
allow_list: Option<&[String]>, | ||
) -> Result<(), Box<dyn Error>> { | ||
info!("Waiting for screen change..."); | ||
let mut prev_screen = self.read()?; | ||
wait_timeout_do(timeout, &mut || { | ||
let screen = self.read()?; | ||
let mut res = None; | ||
for handler in inventory::iter::<HandlerCollector> { | ||
if !handler.inner.can_handle_change(allow_list) { | ||
continue; | ||
} | ||
res = Some( | ||
handler.inner.handle_change(&screen, &prev_screen) || res.unwrap_or(false), | ||
); | ||
} | ||
match res { | ||
Some(true) => Ok(Some(())), | ||
Some(false) => { | ||
prev_screen = screen; | ||
Ok(None) | ||
} | ||
None => Err("No handler found".into()), | ||
} | ||
}) | ||
} | ||
fn wait_still_screen( | ||
&mut self, | ||
timeout: u32, | ||
allow_list: Option<&[String]>, | ||
) -> Result<(), Box<dyn Error>> { | ||
info!("Waiting for still screen..."); | ||
let mut prev_screen = self.read()?; | ||
let changed = wait_timeout_do(timeout, &mut || { | ||
let screen = self.read()?; | ||
let mut res = None; | ||
for handler in inventory::iter::<HandlerCollector> { | ||
if !handler.inner.can_handle_change(allow_list) { | ||
continue; | ||
} | ||
res = Some( | ||
handler.inner.handle_change(&screen, &prev_screen) || res.unwrap_or(false), | ||
); | ||
} | ||
match res { | ||
Some(true) => Ok(Some(())), | ||
Some(false) => { | ||
prev_screen = screen; | ||
Ok(None) | ||
} | ||
None => Err("No handler found".into()), | ||
} | ||
}); | ||
match changed { | ||
Ok(_) => Err("Screen changed".into()), | ||
Err(e) if e.to_string() == "Timeout" => Ok(()), | ||
Err(e) => Err(e), | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
//! Basic handler for GUI. Which also means OpenQA compatible. | ||
|
||
use image::RgbaImage; | ||
|
||
use crate::exec::{ | ||
gui_handler::handler_api::HandlerCollector, | ||
needle::{Area, Needle, NeedleType}, | ||
}; | ||
|
||
use super::handler_api::GuiHandler; | ||
|
||
pub struct BasicHandler {} | ||
|
||
pub fn basic_handle_once(area: &Area, screen: &RgbaImage) -> bool { | ||
let target = match &area.target { | ||
Some(target) => target, | ||
None => return false, | ||
}; | ||
if target.width() != screen.width() || target.height() != screen.height() { | ||
return false; | ||
} | ||
match area.needle { | ||
NeedleType::Match => { | ||
let mut match_count = 0; | ||
let total_count = area.width * area.height; | ||
for x in 0..area.width { | ||
for y in 0..area.height { | ||
let x = area.x + x; | ||
let y = area.y + y; | ||
let pixel = screen.get_pixel(x, y); | ||
let target_pixel = target.get_pixel(x, y); | ||
if pixel == target_pixel { | ||
match_count += 1; | ||
} | ||
} | ||
} | ||
let similarity = match_count as f32 / total_count as f32; | ||
similarity >= area.match_threhold | ||
} | ||
NeedleType::Ocr => return false, | ||
NeedleType::Exclude => { | ||
let mut match_count = 0; | ||
let total_count = screen.width() * screen.height() - area.width * area.height; | ||
for x in 0..screen.width() { | ||
for y in 0..screen.height() { | ||
if x >= area.x | ||
&& x < area.x + area.width | ||
&& y >= area.y | ||
&& y < area.y + area.height | ||
{ | ||
continue; | ||
} | ||
let pixel = screen.get_pixel(x, y); | ||
let target_pixel = target.get_pixel(x, y); | ||
if pixel == target_pixel { | ||
match_count += 1; | ||
} | ||
} | ||
} | ||
let similarity = match_count as f32 / total_count as f32; | ||
similarity >= area.match_threhold | ||
} | ||
} | ||
} | ||
|
||
impl GuiHandler for BasicHandler { | ||
fn can_handle(&self, needle: &Needle) -> bool { | ||
needle.is_basic() | ||
} | ||
fn handle(&self, needle: &Needle, screen: &RgbaImage) -> bool { | ||
let needle = match needle { | ||
Needle::Basic(areas) => areas, | ||
_ => return false, | ||
}; | ||
needle.iter().all(|area| basic_handle_once(area, screen)) | ||
} | ||
fn can_handle_change(&self, allow_list: Option<&[String]>) -> bool { | ||
if let Some(allow_list) = allow_list { | ||
allow_list.iter().any(|x| x == "basic") | ||
} else { | ||
true | ||
} | ||
} | ||
fn handle_change(&self, screen: &RgbaImage, prev_screen: &RgbaImage) -> bool { | ||
for x in 0..screen.width() { | ||
for y in 0..screen.height() { | ||
let pixel = screen.get_pixel(x, y); | ||
let prev_pixel = prev_screen.get_pixel(x, y); | ||
if pixel != prev_pixel { | ||
return false; | ||
} | ||
} | ||
} | ||
true | ||
// screen != prev_screen | ||
} | ||
} | ||
|
||
inventory::submit! { | ||
HandlerCollector { | ||
inner: &BasicHandler {} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
//! Handler API for GUI | ||
use image::RgbaImage; | ||
|
||
use crate::exec::needle::Needle; | ||
|
||
pub trait GuiHandler { | ||
/// Check if this handler can handle this type of needle | ||
fn can_handle(&self, needle: &Needle) -> bool; | ||
/// Handle the needle, return if the screen matches the needle | ||
fn handle(&self, needle: &Needle, screen: &RgbaImage) -> bool; | ||
// Check if this handler can handle screen change | ||
fn can_handle_change(&self, allow_list: Option<&[String]>) -> bool; | ||
/// Handle the screen change, return if the screen matches the previous screen | ||
fn handle_change(&self, screen: &RgbaImage, prev_screen: &RgbaImage) -> bool; | ||
} | ||
|
||
pub type DynGuiHandler = dyn GuiHandler + Send + Sync + 'static; | ||
|
||
pub struct HandlerCollector { | ||
pub inner: &'static DynGuiHandler, | ||
} | ||
|
||
inventory::collect!(HandlerCollector); |
Oops, something went wrong.