#![allow(clippy::or_fun_call)]
#![allow(clippy::unnecessary_operation)] #![allow(clippy::no_effect)]
pub use otter_api_tests::*;
pub use otter_api_tests as apitest;
pub use thirtyfour_sync as t4;
pub use t4::WebDriverCommands;
pub use t4::By;
pub use t4::Keys;
pub type T4d = t4::WebDriver;
pub type WDE = t4::error::WebDriverError;
use t4::Capabilities;
use once_cell::sync::OnceCell;
pub use std::rc::Rc;
#[derive(Debug,Clone,Deref)]
#[derive(StructOpt)]
pub struct Opts {
#[structopt(flatten)] #[deref]
at: apitest::Opts,
#[structopt(long="--layout", default_value="Portrait")]
layout: PresentationLayout,
#[structopt(long="--geckodriver-args", default_value="")]
geckodriver_args: String,
}
impl AsRef<apitest::Opts> for Opts {
fn as_ref(&self) -> &apitest::Opts { &self.at }
}
#[derive(Debug)]
pub struct FinalInfoCollection;
type ScreenShotCount = u32;
type WindowState = Option<String>;
#[derive(Debug,Deref,DerefMut)]
pub struct Setup {
pub opts: Opts,
#[deref] #[deref_mut] pub core: SetupCore,
driver: T4d,
current_window: WindowState,
screenshot_count: ScreenShotCount,
#[allow(dead_code)] final_hook: FinalInfoCollection,
windows_squirreled: Vec<JsLogfile>, }
#[derive(Debug)]
pub struct Window {
pub name: String,
pub player: PlayerId,
pub instance: InstanceName,
pub url: String,
js_logfile: JsLogfile,
vpid_cache: RefCell<HashMap<String, Vpid>>,
}
#[derive(Debug,Clone,Eq,PartialEq,Ord,PartialOrd,Hash)]
#[derive(Deserialize)]
#[serde(transparent)]
pub struct Vpid(pub String);
impl Display for Vpid {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.write_str(&self.0) }
}
impl Window {
pub fn table(&self) -> String { self.instance.to_string() }
}
#[throws(AE)]
fn prepare_xserver(cln: &cleanup_notify::Handle, ds: &DirSubst) {
const DISPLAY: u16 = 12;
let mut xcmd = Command::new("Xvfb");
xcmd
.args("-screen 0 2000x2000x24 \
-nolisten unix \
-nolisten local \
-listen inet \
-listen inet6 \
-terminate \
-retro \
-displayfd 1".split(' '))
.args(&["-fbdir", &ds.abstmp])
.arg(format!(":{}", DISPLAY));
let (l,_) = fork_something_which_prints(xcmd, cln, "Xvfb")?;
if l != DISPLAY.to_string() {
throw!(anyhow!(
"Xfvb said {:?}, expected {:?}",
l, DISPLAY
));
}
let display = format!("[::1]:{}", DISPLAY);
env::set_var("DISPLAY", &display);
let v4display = format!("127.0.0.1:{}", DISPLAY);
let (xconn, _) = x11rb::connect(Some(&v4display))
.context("make keepalive connection to X server")?;
Box::leak(Box::new(xconn));
}
#[throws(AE)]
fn prepare_geckodriver(opts: &Opts, ds: &DirSubst,
cln: &cleanup_notify::Handle) {
const EXPECTED: &str = "Listening on 127.0.0.1:4444";
let mut cmd = Command::new("geckodriver");
cmd.args(&["--binary", &ds.subst("@src@/wdriver/firefox-wrapper")?]);
if opts.geckodriver_args != "" {
cmd.args(opts.geckodriver_args.split(' '));
}
let (l,_) = fork_something_which_prints(cmd, cln, "geckodriver")?;
let fields: Vec<_> = l.split('\t').skip(2).take(2).collect();
let expected = ["INFO", EXPECTED];
if fields != expected {
throw!(anyhow!("geckodriver did not report as expected \
- got {:?}, expected {:?}",
fields, &expected));
}
}
#[throws(AE)]
fn prepare_thirtyfour(ds: &DirSubst)
-> (T4d, ScreenShotCount, Vec<JsLogfile>)
{
let mut count = 0;
let mut caps = t4::DesiredCapabilities::firefox();
let prefs: HashMap<_,_> = [
("devtools.console.stdout.content", true),
].iter().cloned().collect();
caps.add("prefs", prefs)?;
caps.add("stdio", "inherit")?;
let driver = t4::WebDriver::new("http://localhost:4444", &caps)
.context("create 34 WebDriver")?;
const FRONT: &str = "front";
let mut js_logfile = JsLogfileImp::open(ds, FRONT)?;
driver.set_window_name(FRONT).context("set initial window name")?;
screenshot(&driver, &mut count, "startup", log::Level::Trace)?;
driver.get(URL).context("navigate to front page")?;
screenshot(&driver, &mut count, "front", log::Level::Trace)?;
js_logfile.fetch(&driver)?;
let js_logs = vec![Rc::new(RefCell::new(js_logfile))];
let t = Some(5_000 * MS);
driver.set_timeouts(t4::TimeoutConfiguration::new(t,t,t))
.context("set webdriver timeouts")?;
(driver, count, js_logs)
}
pub type JsLogfile = Rc<RefCell<JsLogfileImp>>;
#[derive(Debug)]
pub struct JsLogfileImp {
name: String,
fh: File,
counter: usize,
}
impl JsLogfileImp {
#[throws(AE)]
pub fn open(ds: &DirSubst, name: &str) -> Self {
let path = ds.also(&[("name", name)]).subst(
"@abstmp@/js-@name@.log"
)?;
let name = name.to_owned();
let fh = File::create(&path).with_context(|| path.clone())?;
let counter = 0;
JsLogfileImp { name, fh, counter }
}
pub fn name(&self) -> String {
self.name.clone()
}
#[throws(AE)]
pub fn fetch(&mut self, driver: &T4d) {
self.fetchx(driver, false)?;
}
#[throws(AE)]
pub fn fetchx(&mut self, driver: &T4d, tolerate_errors: bool) {
self.counter += 1;
let head = format!(
"-------------------- JS {} {} --------------------",
&self.name, self.counter
);
writeln!(&mut self.fh, "{}", &head)?;
let mut intolerable = vec![];
(||{
let got = driver.execute_script(r#"
var returning = window.console.saved;
window.console.saved = [];
return returning;
"#).context("get log")?;
for ent in got.value().as_array()
.ok_or(anyhow!("saved isn't an array?"))?
{
#[derive(Deserialize,Debug)]
struct LogEnt(String, Vec<JsV>);
impl fmt::Display for LogEnt {
#[throws(fmt::Error)]
fn fmt(&self, f: &mut fmt::Formatter) {
write!(f, "{}:", self.0)?;
for a in &self.1 { write!(f, " {}", a)?; }
}
}
let ent: LogEnt = serde_json::from_value(ent.clone())
.context("parse log entry")?;
writeln!(&mut self.fh, "{:?}", &ent)?;
if ! tolerate_errors {
match ent.0.as_str() {
"log" => { },
_ => intolerable.push(ent)
}
}
}
Ok::<_,AE>(())
})()
.with_context(|| self.name.clone())
.context("fetch JS log messages")?;
assert!{ intolerable.is_empty(),
"Intolerable JS error(s) {:#?}", intolerable };
info!("{}", head);
}
}
type ScreenCTM = ndarray::Array2::<f64>;
pub struct WindowGuard<'g> {
su: &'g mut Setup,
w: &'g Window,
matrix: OnceCell<ScreenCTM>,
client: OnceCell<String>,
}
impl Debug for WindowGuard<'_> {
#[throws(fmt::Error)]
fn fmt(&self, f: &mut fmt::Formatter) {
f.debug_struct("WindowGuard")
.field("w.name", &self.w.name)
.field("w.instance", &self.w.instance.to_string())
.finish()?
}
}
#[derive(Deserialize,Clone,Debug)]
pub struct WPiece {
pub piece: Vpid,
pub p: JsV,
}
pub trait LogIgnoreBefore {
fn matches(&mut self, s: &str) -> bool;
}
type LogIgnoreBeforeFn<'r> = &'r mut dyn FnMut(&str) -> bool;
impl<'r> LogIgnoreBefore for LogIgnoreBeforeFn<'r> {
fn matches(&mut self, s: &str) -> bool { self(s) }
}
impl LogIgnoreBefore for HtmlLit {
fn matches(&mut self, s: &str) -> bool { s.contains(self.as_html_str()) }
}
impl LogIgnoreBefore for Generation {
fn matches(&mut self, s: &str) -> bool {
s == synch_logentry(*self).as_html_str()
}
}
impl<'g> WindowGuard<'g> {
#[throws(AE)]
pub fn piece_vpid(&'g self, some_pieceid: &'_ str) -> Vpid {
if some_pieceid.contains('.') { return Vpid(some_pieceid.to_owned()) }
let mut cache = self.w.vpid_cache.borrow_mut();
if let Some(got) = cache.get(some_pieceid) { return got.clone() }
let (l, r) = some_pieceid.split_once('v').unwrap();
let s = format!(r#"{{ "idx":{}, "version":{} }}"#, l,r); let kd: slotmap::KeyData = serde_json::from_str(&s).unwrap();
let piece: PieceId = kd.into();
let resp = self.su.mgmt_conn().cmd(&MC::AlterGame {
game: TABLE.parse().unwrap(),
how: MgmtGameUpdateMode::Online,
insns: vec![ MgmtGameInstruction::PieceIdLookupFwd {
piece,
player: self.w.player,
} ],
})?;
let vpid = if_chain!{
if let MgmtResponse::AlterGame { error: None, responses } = &resp;
if let [MgmtGameResponse::VisiblePieceId(vpid)] = responses.as_slice();
then { vpid }
else { unreachable(Err::<Void,_>(&resp).unwrap()) }
};
let got = Vpid(vpid.unwrap().to_string());
cache.insert(some_pieceid.to_owned(), got.clone());
got
}
pub fn vpid_clear_cache(&'g self) {
self.w.vpid_cache.borrow_mut().clear();
}
#[throws(AE)]
pub fn vpidelem(&'g self, prefix: &'_ str, some_pieceid: &'_ str) -> String {
prefix.to_string() + &self.piece_vpid(some_pieceid)?.0
}
#[throws(AE)]
pub fn find_piece(&'g self, pieceid: &'_ str) -> PieceElement<'g> {
let pieceid = self.piece_vpid(pieceid)?;
let elemid = format!("use{}", &pieceid);
let elem = self.su.driver.find_element(By::Id(&elemid))?;
PieceElement {
elem, pieceid,
w: self,
}
}
#[throws(AE)]
pub fn client(&mut self) -> String {
let us = self.client.get_or_try_init(||{
let us = self.execute_script(r##"return us;"##)?;
let us = us.value();
let us = us.as_str().ok_or_else(
|| anyhow!("return us script gave {:?}", &us))?;
Ok::<_,AE>(us.to_owned())
}).context("obtain client ID")?;
us.clone()
}
#[throws(AE)]
pub fn piece_held(&self, pc: &str) -> Option<String> {
let held = self.execute_script(&format!(r##"
let pc = pieces['{}'];
return pc.held;
"##, &self.piece_vpid(pc)?))?;
let held = held.value();
dbg!(held);
match held {
JsV::Null => None,
JsV::String(s) => Some(s.to_owned()),
_ => Err(anyhow!("held check script gave {:?}", held))?,
}
}
#[throws(AE)]
pub fn posg2posw(&self, posg: Pos) -> WebPos {
let mat = self.matrix.get_or_try_init(||{
let ary = self.su.driver.execute_script(r#"
let m = space.getScreenCTM();
return [m.a, m.b, m.c, m.d, m.e, m.f];
"#)?;
let ary = ary.value();
dbg!(ary);
let mat = (||{
let ary = ary.as_array().ok_or_else(|| anyhow!("not array"))?;
let mut mat: ScreenCTM = ndarray::Array2::zeros((3,3));
for got in itertools::Itertools::zip_longest(
[11, 12, 21, 22, 41, 42].iter(),
ary.iter(),
) {
let (mij, v) = got.both().ok_or_else(|| anyhow!("wrong length"))?;
let adj = |v| (if v == 4 { 3 } else { v }) - 1;
let i = adj(mij / 10);
let j = adj(mij % 10);
mat[(j,i)] = v.as_f64().ok_or_else(|| anyhow!("entry not f64"))?;
}
Ok::<_,AE>(mat)
})()
.with_context(|| format!("getScreenCGM script gave {:?}", &ary))?;
dbg!(&mat);
Ok::<_,AE>(mat)
})?;
(||{
let vec: ndarray::Array1<f64> =
posg.coords.iter()
.cloned()
.map(|v| v as f64)
.chain(iter::once(1.))
.collect();
dbg!(&vec);
let vec = mat.dot(&vec);
let mut coords = vec.iter().map(
|v| NumCast::from(v.round()).ok_or_else(
|| anyhow!("coordinate {} out of range in {:?}", v, &vec))
);
let mut coord = || coords.next().unwrap();
Ok::<WebPos,AE>((
coord()?,
coord()?,
))
})()
.context("convert game position to web page coordinates")?
}
#[throws(AE)]
pub fn retrieve_log<I: LogIgnoreBefore>(&self, mut ignore_before: I)
-> Vec<String>
{
#[throws(AE)]
fn inner<'g, 'i>
(w: &'g WindowGuard, ignore_before: LogIgnoreBeforeFn<'i>)
-> Vec<String>
{
let log = w.find_elements(By::ClassName("logmsg"))?;
let log = log.iter()
.rev()
.map(|e| e.inner_html())
.take_while(|h| {
h.as_ref().ok()
.map(|s: &String| ignore_before(s))
!= Some(true)
})
.collect::<Result<Vec<String>,_>>()?;
assert!( ! log.is_empty() );
dbg!(log)
}
inner(self, &mut move |s| ignore_before.matches(s))?
}
#[throws(AE)]
pub fn fetch_js_log(&self) {
self.w.js_logfile.borrow_mut().fetch(&self.su.driver)?
}
}
#[ext(pub)]
impl Vec<String> {
fn find_conflicts(&self) -> Vec<String> {
self.iter().filter(|m| m.starts_with("Conflict!"))
.cloned().collect()
}
fn assert_no_conflicts(&self) {
assert_eq!(self.find_conflicts(), vec![] as Vec<String>);
}
}
pub type WebCoord = i32;
pub type WebPos = (WebCoord, WebCoord);
#[derive(Deref)]
pub struct PieceElement<'g> {
pieceid: Vpid,
w: &'g WindowGuard<'g>,
#[deref] elem: t4::WebElement<'g>,
}
impl<'g> PieceElement<'g> {
#[throws(AE)]
pub fn posg(&self) -> Pos {
(||{
let a = |a| Ok::<_,AE>(
self.get_attribute(a)?.ok_or(anyhow!("{}", a))?.parse()?
);
let x = a("x")?;
let y = a("y")?;
Ok::<_,AE>(PosC::new(x,y))
})()
.with_context(|| self.pieceid.to_owned())
.context("read position of piece out of x,y attributes")?
}
#[throws(AE)]
pub fn posw(&self) -> WebPos {
let posg = self.posg()?;
self.w.posg2posw(posg)
.with_context(|| self.pieceid.to_owned())?
}
}
impl<'g> TryInto<WebPos> for &'g PieceElement<'g> {
type Error = AE;
#[throws(AE)]
fn try_into(self) -> WebPos {
self.posw()?
}
}
#[throws(AE)]
fn check_window_name_sanity(name: &str) -> &str {
let e = || anyhow!("bad window name {:?}", &name);
name.chars().nth(0).ok_or(e())?
.is_ascii_alphanumeric().ok_or(e())?;
name.chars().all(
|c| c.is_ascii_alphanumeric() || c == '-' || c == '_'
).ok_or(e())?;
name
}
impl Setup {
#[throws(AE)]
pub fn new_window<'s>(&'s mut self, instance: &Instance, name: &str,
player: PlayerId, url: String)
-> Window {
let name = check_window_name_sanity(name)?;
let window = (||{
self.current_window = None;
match self.driver.switch_to().window_name(name) {
Ok(()) => throw!(anyhow!("window already exists")),
Err(WDE::NoSuchWindow(_)) |
Err(WDE::NotFound(..)) => (),
e@ Err(_) => {
eprintln!("wot {:?}", &e);
throw!(e
.context("check for pre-existing window")
.err().unwrap())
},
};
self.driver.execute_script(&format!(
r#"window.open('', target='{}');"#,
name,
))
.context("execute script to create window")?;
let js_logfile = JsLogfileImp::open(&self.core.ds, name)?;
let js_logfile = Rc::new(RefCell::new(js_logfile));
self.windows_squirreled.push(js_logfile.clone());
Ok::<_,AE>(Window {
name: name.to_owned(),
instance: instance.0.clone(),
vpid_cache: default(),
url, player, js_logfile,
})
})()
.with_context(|| name.to_owned())
.context("create window")?;
window
}
}
impl Setup {
#[throws(AE)]
pub fn w<'s>(&'s mut self, w: &'s Window) -> WindowGuard<'s> {
if self.current_window.as_ref() != Some(&w.name) {
self.driver.switch_to().window_name(&w.name)
.with_context(|| w.name.to_owned())
.context("switch to window")?;
self.current_window = Some(w.name.clone());
}
WindowGuard {
w,
su: self,
matrix: default(),
client: default(),
}
}
}
impl<'g> Deref for WindowGuard<'g> {
type Target = T4d;
fn deref(&self) -> &T4d { &self.su.driver }
}
impl<'g> Drop for WindowGuard<'g> {
fn drop(&mut self) {
(|| Ok::<_,AE>(
self.w.js_logfile.try_borrow_mut().context("borrow js log")?
.fetch(&self.su.driver)?
))().just_warn();
}
}
pub trait Screenshottable {
fn screenshot(&mut self, slug: &str, level: log::Level) -> Result<(),AE>;
}
impl<'g> Screenshottable for WindowGuard<'g> {
#[throws(AE)]
fn screenshot(&mut self, slug: &str, level: log::Level) {
screenshot(&self.su.driver, &mut self.su.screenshot_count,
&format!("{}-{}", &self.w.name, slug), level)?
}
}
#[throws(AE)]
fn screenshot(driver: &T4d, count: &mut ScreenShotCount, slug: &str,
level: log::Level) {
if !log_enabled!(level) {
debug!("skipping screenshot {}", slug);
return
}
let path = format!("{:03}{}.png", count, slug);
*count += 1;
driver.screenshot(&PathBuf::from(&path))
.with_context(|| path.clone())
.context("take screenshot")?;
debug!("screenshot {}", &path);
}
impl<'g> WindowGuard<'g> {
#[throws(AE)]
pub fn otter(&mut self, verb: &[&str], args: &[&str]) {
let args: Vec<String> =
["--account", "server:", "--game", &self.w.table()]
.iter().cloned().map(Into::into)
.chain(verb.iter().cloned().map(Into::into))
.chain(args.iter().cloned().map(Into::into))
.collect();
self.su.ds.otter(&args)?;
}
#[throws(AE)]
fn synch_raw(&mut self) -> Generation {
let gen = self.su.mgmt_conn().game_synch(self.w.instance.clone())?;
(|| {
loop {
info!("{:?} gen={}", self, gen);
let tgen = self.su.driver.execute_async_script(
&Subst::from(&[
("wanted", &gen.to_string())
]).subst(r#"
var done = arguments[0];
function no_queue() { return !api_queue.length && !api_posting; }
if (gen >= @wanted@ && no_queue()) { done(gen); return; }
window.test_update_hook = function() {
window.test_update_hook = function() { };
done(no_queue() ? gen : 0);
};
"#)?
)
.context("run async script")?
.value().as_u64().ok_or(anyhow!("script return is not u64"))?;
let tgen = Generation(tgen);
info!("{:?} gen={} tgen={}", self, gen, tgen);
if tgen >= gen { break; }
}
Ok::<(),AE>(())
})()
.context("await gen update via async js script")?;
gen
}
#[throws(AE)]
pub fn synch_ignore_js_errors(&mut self) -> Generation {
let gen = self.synch_raw()?;
self.su.driver.execute_script(r#"
let e = document.getElementById('error');
e.innerHTML = "";
"#)
.context("clear in-client trapped errors")?;
gen
}
#[throws(AE)]
pub fn synch(&mut self) -> Generation {
let gen = self.synch_raw()?;
(|| {
let errors = self.su.driver.execute_script(r#"
let e = document.getElementById('error');
if (e) {
return e.innerHTML;
} else {
console.log('wdt-*: no errors element, no trapped errors check');
return "";
}
"#)
.context("get errors")?;
let errors = errors
.value()
.as_str()
.ok_or_else(|| anyhow!("errors script gave non-string"))?;
if ! errors.is_empty() {
throw!(anyhow!("JS errors - HTML: {}", errors));
}
Ok::<(),AE>(())
})()
.context("check for in-client trapped errors")?;
gen
}
#[throws(AE)]
pub fn pieces(&mut self) -> Vec<WPiece> {
self.su.driver.execute_script(r#"
let uelem = pieces_marker;
let out = [];
for (;;) {
uelem = uelem.nextElementSibling;
let piece = uelem.dataset.piece;
if (!piece) break;
let p = pieces[piece];
out.push({ piece: piece, p: p });
}
return out;
"#)
.did("fetch ids")?
.convert()?
}
}
impl Drop for FinalInfoCollection {
fn drop(&mut self) {
nix::unistd::linkat(None, "Xvfb_screen0",
None, "Xvfb_keep.xwd",
unistd::LinkatFlags::NoSymlinkFollow)
.context("preserve Xvfb screen")
.just_warn();
}
}
pub trait IntoInWindow<T> {
fn w_into<'g>(self, w: &'g WindowGuard) -> Result<T, AE>;
}
impl<T> IntoInWindow<T> for T {
#[throws(AE)]
fn w_into<'g>(self, _w: &'g WindowGuard) -> T { self }
}
impl IntoInWindow<WebPos> for Pos {
#[throws(AE)]
fn w_into<'g>(self, w: &'g WindowGuard) -> WebPos {
w.posg2posw(self)?
}
}
#[ext(pub, name=ActionChainExt, supertraits=Sized)]
impl<'a> t4::action_chain::ActionChain<'a> {
#[throws(AE)]
fn move_w<'g, P: Debug + Copy + IntoInWindow<WebPos>>
(self, w: &'g WindowGuard, pos: P) -> Self
{
let pos: WebPos = pos.w_into(w)
.with_context(|| format!("{:?}", pos))
.context("find coordinate")?;
self.move_pos(pos)?
}
#[throws(AE)]
fn move_pos<'g,
E,
P: TryInto<WebPos, Error=E>>
(self, pos: P) -> Self
where Result<WebPos,E>: anyhow::Context<WebPos,E>
{
let (px,py) = pos.try_into().context("convert")?;
trace!("move_pos: ({}, {})", px, py);
self.move_to(px,py)
}
#[throws(AE)]
fn move_pc<'g>(self, w: &'g WindowGuard, pc: &'_ str) -> Self {
(||{
let p = w.find_piece(pc).context("find")?;
let pos = p.posw().context("get pos")?;
let r = self.move_pos(pos).context("move")?;
Ok::<_,AE>(r)
})().with_context(|| format!("piece {}", pc))?
}
}
impl Drop for Setup {
fn drop(&mut self) {
(||{
for jslog in mem::take(&mut self.windows_squirreled) {
let name = jslog.try_borrow()?.name();
let w = Window {
name: name.clone(),
instance: TABLE.parse().context(TABLE)?,
player: default(),
vpid_cache: default(),
js_logfile: jslog.clone(),
url: default(),
};
self.w(&w)?.screenshot("final", log::Level::Info)
.context(name)
.context("final screenshot")
.just_warn();
}
Ok::<_,AE>(())
})()
.context("screenshots, in Setup::drop")
.just_warn();
}
}
#[throws(AE)]
pub fn setup(exe_module_path: &str) -> (Setup, Instance) {
let (opts, instance, core) =
apitest::setup_core(
&[exe_module_path, "otter_webdriver_tests"],
)?;
prepare_xserver(&core.cln, &core.ds).did("setup X server")?;
let final_hook = FinalInfoCollection;
prepare_geckodriver(&opts, &core.ds, &core.cln).did("setup webdriverr")?;
let (driver, screenshot_count, windows_squirreled) =
prepare_thirtyfour(&core.ds).did("prepare web session")?;
(Setup {
core,
driver,
opts,
screenshot_count,
current_window: None,
windows_squirreled,
final_hook,
}, instance)
}
impl Setup {
#[throws(AE)]
pub fn setup_static_users(&mut self, instance: &Instance) -> Vec<Window> {
let susus = self.core.ds.clone()
.setup_static_users(&mut self.mgmt_conn(), self.opts.layout)?;
susus
.into_iter().map(|sus|
{
let w = self.new_window(instance, sus.nick,
sus.player, sus.url.clone())?;
self.w(&w)?.get(sus.url)?;
self.w(&w)?.screenshot("initial", log::Level::Trace)?;
Ok::<_,AE>(w)
})
.collect::<Result<_,_>>()?
}
}
pub struct UsualSetup {
pub su: Setup,
pub inst: Instance,
pub alice: Window,
pub bob: Window,
pub spec: GameSpec,
}
impl UsualSetup {
#[throws(AE)]
pub fn new(exe_module_path: &str) -> UsualSetup {
let (mut su, inst) = setup(exe_module_path).did("usual setup")?;
let [alice, bob] : [Window; 2] =
su.setup_static_users(&inst)?.try_into().unwrap();
let spec = su.ds.game_spec_data()?;
UsualSetup { su, inst, alice, bob, spec }
}
}
#[throws(Explode)]
pub fn as_usual<F: FnOnce(UsualSetup) -> Result<(), Explode>>(
f: F, exe_module_path: &str,
) {
let usual = UsualSetup::new(exe_module_path)?;
f(usual)?;
info!("ok");
}
portmanteau_has!("wdt-altergame.rs", wdt_altergame);
portmanteau_has!("wdt-hand.rs", wdt_hand);
portmanteau_has!("wdt-simple.rs", wdt_simple);
portmanteau_has!("wdt-bundles.rs", wdt_bundles);
#[throws(AE)]
fn main() { portmanteau_main("wdt")? }