diff --git a/Cargo.toml b/Cargo.toml index 25f07e8..9cf386d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,3 +22,19 @@ jid = { version = "*" } [dev-dependencies] syntect = "5.0" diff = "0.1" +scansion = { version = "*" } + +[build-dependencies] +jid = { version = "*", features = ["minidom"] } +minidom = { version = "*" } +quote = "1.0" +rand = "0.8" +scansion = { version = "*" } + +[patch.crates-io] +sasl = { git = "https://code.bouah.net/pep/xmpp-rs.git" } +jid = { git = "https://code.bouah.net/pep/xmpp-rs.git", branch = "jid-quote" } +minidom = { git = "https://code.bouah.net/pep/xmpp-rs.git" } +xmpp-parsers = { git = "https://code.bouah.net/pep/xmpp-rs.git" } +tokio-xmpp = { git = "https://code.bouah.net/pep/xmpp-rs.git" } +scansion = { git = "https://code.bouah.net/pep/scansion-rs", branch = "quote" } diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..f07018a --- /dev/null +++ b/build.rs @@ -0,0 +1,201 @@ +// Copyright (C) 2023-2099 The crate authors. +// +// This program is free software: you can redistribute it and/or modify it +// under the terms of the GNU Affero General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at your +// option) any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License +// for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +use std::ffi::OsString; +use std::fmt::Display; +use std::fs::File; +use std::io::{self, BufWriter, Read, Write}; +use std::path::{Path, PathBuf}; + +use minidom::Error as MinidomError; +use quote::{format_ident, quote, TokenStreamExt}; +use scansion::{read_actions_component, read_spec, Spec}; + +fn generate_spec_tokens(spec: Spec) -> Result { + println!("FOO0"); + + let docstr = { + let mut tmp = String::new(); + if let Some(ref meta) = spec.metadata { + tmp.push_str(meta.title.as_str()); + tmp.push('\n'); + + if let Some(ref desc) = meta.description { + tmp.push_str(desc.as_str()); + tmp.push('\n'); + } + + for tag in &meta.tags { + tmp.push_str("tag: "); + tmp.push_str(tag.as_str()); + tmp.push('\n'); + } + } else { + tmp.push_str("No metadata"); + } + + tmp + }; + + let context = spec.context.clone(); + let actions = read_actions_component(spec, &context).unwrap(); + + let stanzas_in = actions + .inbound + .into_iter() + .map(|elem| { + // TODO: Prevent having to parse elements again. + // We do need some kind of structure already though to be able to verify the presence + // of attrs and all before the generation. + let elem_str = String::from(&elem); + quote! { ScanElement::new(#elem_str.parse::().unwrap()).apply_context(&context) } + }) + .collect::>(); + + let stanzas_out = actions + .outbound + .into_iter() + .map(|elem| { + let elem_str = String::from(&elem); + quote! { + component.expect( + #elem_str.parse::().unwrap().apply_context(&context) + ); + } + }) + .collect::>(); + + println!("FOO1"); + + let (context_keys, context_values) = context + .into_iter() + .fold((vec![], vec![]), |(mut keys, mut vals), (k, v)| { + println!("BAR0"); + keys.push(quote! { #k }); + println!("BAR1"); + vals.push(quote! { #v }); + println!("BAR2: {:?}; {:?}", keys, vals); + (keys, vals) + }); + + println!("FOO2"); + + Ok(quote! { + #![doc = #docstr] + use crate::component::TestComponent; + use crate::handlers::handle_stanza; + use crate::room::Room; + + use ::std::collections::HashMap; + use ::xmpp_parsers::{Jid, BareJid, FullJid, Element}; + use ::scansion::{ScanElement, Entity, Client}; + + #[tokio::test] + async fn spec() { + let context: ::scansion::Context = { + let tmp = ::std::collections::HashMap::new(); + let (keys, values) = (vec![#(#context_keys),*], vec![#(#context_values),*]); + for (k, v) in keys.iter().zip(values.iter()) { + tmp.insert(String::from(*k), v.clone()); + } + tmp + }; + + let stanzas_in = vec![#(#stanzas_in),*]; + let mut component = TestComponent::new_scan(stanzas_in); + let mut rooms: HashMap = HashMap::new(); + + #(#stanzas_out)* + + println!("FOO: {component:?}"); + handle_stanza(&mut component, &mut rooms).await.unwrap(); + } + }) +} + +fn generate_mod_tokens(modfiles: Vec) -> impl TokenStreamExt + Display { + let modfiles: Vec<_> = modfiles + .into_iter() + .map(|s| format_ident!("{}", s.into_string().unwrap().strip_suffix(".rs").unwrap())) + .collect(); + quote! { + /// Scansion tests module. + /// These tests are generated by the build script, DO NOT EDIT. + + #(#[cfg(test)] mod #modfiles;)* + } +} + +fn read_input_dir(indir: PathBuf, outdir: PathBuf) -> io::Result> { + // Will be used to generate mod.rs + let mut modfiles: Vec = Vec::new(); + + for entry in indir.read_dir()? { + let mut p = entry?.path(); + match p.extension() { + Some(ext) if ext == "scs" => (), + _ => continue, + } + + let mut infile = File::open(p.clone())?; + let mut contents = String::new(); + infile.read_to_string(&mut contents)?; + + let spec = read_spec(&contents); + match spec { + Ok(_) => println!("Path: {p:?}: \x1b[32m OK\x1b[0m"), + Err(err) => { + println!("Path: {p:?}: \x1b[31mERR\x1b[0m\n{err:?}"); + continue; + } + } + + // Path is now only going to be used as .rs + p.set_extension("rs"); + + let filename = OsString::from(p.file_name().unwrap()); + let outpath = outdir.join(filename.clone()); + println!("Outpath: {outpath:?}"); + + let tokens = generate_spec_tokens(spec.unwrap()).unwrap(); + let mut output = BufWriter::new(File::create(&outpath)?); + write!(output, "{}", tokens)?; + + // Add to the set of files for which generation succeeded to then generate mod.rs + modfiles.push(filename); + } + + Ok(modfiles) +} + +fn main() -> io::Result<()> { + let indir = Path::new("./specs"); + let outdir = Path::new("./src/tests/scansion"); + + if !indir.is_dir() || !outdir.is_dir() { + return Err(io::Error::new( + io::ErrorKind::Other, + "Input and output paths must be directories.", + )); + } + + let modfiles = read_input_dir(indir.to_path_buf(), outdir.to_path_buf())?; + + let mut modout = BufWriter::new(File::create(outdir.join("mod.rs"))?); + let tokens = generate_mod_tokens(modfiles); + write!(modout, "{}", tokens)?; + + Ok(()) +} diff --git a/src/component/test.rs b/src/component/test.rs index c6184cb..8e32ebc 100644 --- a/src/component/test.rs +++ b/src/component/test.rs @@ -28,6 +28,7 @@ use async_trait::async_trait; use diff; use futures::{task::Poll, Stream}; use log::debug; +use scansion::ScanElement; use syntect::{ easy::HighlightLines, highlighting::{Style, ThemeSet}, @@ -59,17 +60,23 @@ impl fmt::Debug for Expect { } } -#[derive(Clone, Eq, PartialEq)] -pub struct TestElement(pub Element); +#[derive(Clone)] +pub struct TestElement(pub ScanElement); impl Deref for TestElement { - type Target = Element; + type Target = ScanElement; fn deref(&self) -> &Self::Target { &self.0 } } +impl PartialEq for TestElement { + fn eq(&self, other: &Element) -> bool { + self.0 == *other + } +} + impl fmt::Debug for TestElement { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", String::from(&self.0)) @@ -90,31 +97,43 @@ impl From<&TestElement> for String { impl From for TestElement { fn from(elem: Element) -> Self { - Self(elem) + Self(ScanElement::new(elem)) } } impl From for Element { fn from(elem: TestElement) -> Self { - elem.0 + elem.0.elem } } impl From for TestElement { fn from(elem: Iq) -> Self { - Self(Element::from(elem)) + Self(Element::from(elem).into()) } } impl From for TestElement { fn from(elem: Presence) -> Self { - Self(Element::from(elem)) + Self(Element::from(elem).into()) } } impl From for TestElement { fn from(elem: Message) -> Self { - Self(Element::from(elem)) + Self(Element::from(elem).into()) + } +} + +impl From for TestElement { + fn from(scan: ScanElement) -> Self { + Self(scan) + } +} + +impl From for ScanElement { + fn from(elem: TestElement) -> ScanElement { + ScanElement::new(elem.into()) } } @@ -132,7 +151,21 @@ impl Component { in_buffer: VecDeque::from( in_buffer .into_iter() - .map(|el| TestElement(el)) + .map(|elem| TestElement::from(elem)) + .collect::>(), + ), + expect_buffer: HashMap::new(), + } + } + + pub fn new_scan(in_buffer: Vec) -> Self { + let _ = env_logger::builder().is_test(true).try_init(); + + Component { + in_buffer: VecDeque::from( + in_buffer + .into_iter() + .map(|elem| TestElement::from(elem)) .collect::>(), ), expect_buffer: HashMap::new(), @@ -358,7 +391,7 @@ impl Stream for Component { fn poll_next(mut self: Pin<&mut Self>, _cx: &mut Context) -> Poll> { while self.in_buffer.len() > 0 { - return Poll::Ready(self.in_buffer.pop_front().map(|el| el.0)); + return Poll::Ready(self.in_buffer.pop_front().map(|el| el.0.elem)); } Poll::Ready(None) @@ -370,11 +403,25 @@ impl Drop for Component { // Don't assert if we're already panicking. Rustc displays a huge backtrace when "panicked // while panicking" even when nobody asks for it (RUST_BACKTRACE unset). Let the error // appear if there isn't any other error. - if !thread::panicking() { - assert!( - self.expect_buffer.is_empty(), - "Remaining expected elements in the buffer" - ); + if !thread::panicking() && !self.expect_buffer.is_empty() { + let mut buffer = String::new(); + for (_stream, buf) in &self.expect_buffer { + for expect in buf { + match expect { + Expect::Element(el) => { + let elem = String::from(el); + buffer + .push_str(format!("expected: `{}`", Component::hl(&elem)).as_str()); + } + expect_cb => { + buffer.push_str(format!("expected: {:?}", expect_cb).as_str()); + } + } + } + } + + debug!("\n{}", buffer); + panic!("Remaining expected elements in the buffer"); } } } diff --git a/src/room.rs b/src/room.rs index 0b62f61..f0f2824 100644 --- a/src/room.rs +++ b/src/room.rs @@ -462,14 +462,14 @@ impl Room { let presence_join = Presence::new(PresenceType::None) .with_from(Jid::Full(new_session.participant().clone())); - let presence_join_to_others = presence_join.clone().with_payloads(vec![MucUser { + let _presence_join_to_others = presence_join.clone().with_payloads(vec![MucUser { status: vec![], items: vec![MucItem::new(Affiliation::Owner, Role::Moderator)], } .into()]); let occupant = self.get_occupant(&new_session)?; - let mucuser = MucUser { + let _mucuser = MucUser { status: vec![], items: { occupant @@ -482,6 +482,16 @@ impl Room { }, }; + self.broadcast_presence( + component, + self.get_occupant(&new_session)?, + new_session.presence.clone(), + BroadcastPresence::Update, + None, + ) + .await?; + + /* for (nick, occupant) in self.occupants.iter() { for session in occupant.iter() { // Self occupant @@ -522,6 +532,7 @@ impl Room { } } } + */ Ok(()) } diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 33e80a5..a47c2d6 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -16,5 +16,6 @@ mod iq; mod presence; mod presence_msn; +mod scansion; #[allow(dead_code)] pub mod templates; diff --git a/src/tests/scansion/.keep b/src/tests/scansion/.keep new file mode 100644 index 0000000..e69de29 diff --git a/src/tests/scansion/mod.rs b/src/tests/scansion/mod.rs new file mode 100644 index 0000000..5ea95ec --- /dev/null +++ b/src/tests/scansion/mod.rs @@ -0,0 +1,6 @@ +#[doc = r" Scansion tests module."] +#[doc = r" These tests are generated by the build script, DO NOT EDIT."] +#[cfg(test)] +mod muc_create_destroy; +#[cfg(test)] +mod muc_creation; diff --git a/src/tests/scansion/muc_create_destroy.rs b/src/tests/scansion/muc_create_destroy.rs new file mode 100644 index 0000000..5a8f668 --- /dev/null +++ b/src/tests/scansion/muc_create_destroy.rs @@ -0,0 +1,51 @@ +#![doc = "MUC creation, basic messages and destruction\n"] +use crate::component::TestComponent; +use crate::handlers::handle_stanza; +use crate::room::Room; +use scansion::ScanElement; +use std::collections::HashMap; +use xmpp_parsers::{BareJid, Element}; +#[tokio::test] +async fn spec() { + println!("FOO0: {:?}", "Admin\nJuliet\nRomeo\n"); + println!( + "FOO0: {:?}", + "admin@localhost/DfNgg9VE\njuliet@localhost/lVwkim_k\nromeo@localhost/mK0dD6Ha\n" + ); + let stanzas_in = vec ! [ScanElement :: new ("\n\t\t\n\t" . parse :: < Element > () . unwrap ()) . with_context (Some (& context)) , ScanElement :: new ("\n\t\t\n\t\t\t\n\t\t\n\t" . parse :: < Element > () . unwrap ()) . with_context (Some (& context)) , ScanElement :: new ("\n\t\tWhere are thou my Juliet?\n\t" . parse :: < Element > () . unwrap ()) . with_context (Some (& context)) , ScanElement :: new ("\n\t\t\n\t" . parse :: < Element > () . unwrap ()) . with_context (Some (& context)) , ScanElement :: new ("\n\t\t/me jumps out from behind a tree\n\t" . parse :: < Element > () . unwrap ()) . with_context (Some (& context)) , ScanElement :: new ("\n\t\tHere I am!\n\t" . parse :: < Element > () . unwrap ()) . with_context (Some (& context)) , ScanElement :: new ("\n\t\tWhat is this place?\n\t" . parse :: < Element > () . unwrap ()) . with_context (Some (& context)) , ScanElement :: new ("\n\t\tI think we're in a script!\n\t" . parse :: < Element > () . unwrap ()) . with_context (Some (& context)) , ScanElement :: new ("\n\t\tOh no! Does that mean our love is not real?!\n\t" . parse :: < Element > () . unwrap ()) . with_context (Some (& context)) , ScanElement :: new ("\n\t\tI refuse to accept this! Let's burn this place to the ground!\n\t" . parse :: < Element > () . unwrap ()) . with_context (Some (& context)) , ScanElement :: new ("\n\t\tYes!\n\t" . parse :: < Element > () . unwrap ()) . with_context (Some (& context)) , ScanElement :: new ("\n\t\t\n\t\t\t\n\t\t\t\tWe refuse to live in this fantasy!\n\t\t\t\n\t\t\n\t" . parse :: < Element > () . unwrap ()) . with_context (Some (& context)) , ScanElement :: new ("\n\t\t\n\t" . parse :: < Element > () . unwrap ()) . with_context (Some (& context)) , ScanElement :: new ("\n\t\t\n\t\t\t\n\t\t\n\t" . parse :: < Element > () . unwrap ()) . with_context (Some (& context)) , ScanElement :: new ("\n\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\telsewhere@conference.localhost\n\t\t\t\t\n\t\t\t\n\t\t\n\t" . parse :: < Element > () . unwrap ()) . with_context (Some (& context))] ; + let mut component = TestComponent::new_scan(stanzas_in); + let mut rooms: HashMap = HashMap::new(); + component . expect ("\n\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t" . parse :: < ScanElement > () . unwrap () . with_context (Some (& context))) ; + component . expect ("\n\t\t\n\t" . parse :: < ScanElement > () . unwrap () . with_context (Some (& context))) ; + component . expect ("" . parse :: < ScanElement > () . unwrap () . with_context (Some (& context))) ; + component . expect ("\n\t\tWhere are thou my Juliet?\n\t" . parse :: < ScanElement > () . unwrap () . with_context (Some (& context))) ; + component . expect ("\n\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\n\t" . parse :: < ScanElement > () . unwrap () . with_context (Some (& context))) ; + component . expect ("\n\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t" . parse :: < ScanElement > () . unwrap () . with_context (Some (& context))) ; + component . expect ("\n\t\tWhere are thou my Juliet?\n\t\t\n\t" . parse :: < ScanElement > () . unwrap () . with_context (Some (& context))) ; + component . expect ("\n\t\t\n\t" . parse :: < ScanElement > () . unwrap () . with_context (Some (& context))) ; + component . expect ("\n\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\n\t" . parse :: < ScanElement > () . unwrap () . with_context (Some (& context))) ; + component . expect ("\n\t\t/me jumps out from behind a tree\n\t" . parse :: < ScanElement > () . unwrap () . with_context (Some (& context))) ; + component . expect ("\n\t\t/me jumps out from behind a tree\n\t" . parse :: < ScanElement > () . unwrap () . with_context (Some (& context))) ; + component . expect ("\n\t\tHere I am!\n\t" . parse :: < ScanElement > () . unwrap () . with_context (Some (& context))) ; + component . expect ("\n\t\tHere I am!\n\t" . parse :: < ScanElement > () . unwrap () . with_context (Some (& context))) ; + component . expect ("\n\t\tWhat is this place?\n\t" . parse :: < ScanElement > () . unwrap () . with_context (Some (& context))) ; + component . expect ("\n\t\tWhat is this place?\n\t" . parse :: < ScanElement > () . unwrap () . with_context (Some (& context))) ; + component . expect ("\n\t\tI think we're in a script!\n\t" . parse :: < ScanElement > () . unwrap () . with_context (Some (& context))) ; + component . expect ("\n\t\tI think we're in a script!\n\t" . parse :: < ScanElement > () . unwrap () . with_context (Some (& context))) ; + component . expect ("\n\t\tOh no! Does that mean our love is not real?!\n\t" . parse :: < ScanElement > () . unwrap () . with_context (Some (& context))) ; + component . expect ("\n\t\tOh no! Does that mean our love is not real?!\n\t" . parse :: < ScanElement > () . unwrap () . with_context (Some (& context))) ; + component . expect ("\n\t\tI refuse to accept this! Let's burn this place to the ground!\n\t" . parse :: < ScanElement > () . unwrap () . with_context (Some (& context))) ; + component . expect ("\n\t\tI refuse to accept this! Let's burn this place to the ground!\n\t" . parse :: < ScanElement > () . unwrap () . with_context (Some (& context))) ; + component . expect ("\n\t\tYes!\n\t" . parse :: < ScanElement > () . unwrap () . with_context (Some (& context))) ; + component . expect ("\n\t\tYes!\n\t" . parse :: < ScanElement > () . unwrap () . with_context (Some (& context))) ; + component . expect ("\n\t\t\n\t\t\t\n\t\t\t\tWe refuse to live in this fantasy!\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t" . parse :: < ScanElement > () . unwrap () . with_context (Some (& context))) ; + component . expect ("\n\t\t\n\t\t\t\n\t\t\t\tWe refuse to live in this fantasy!\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t" . parse :: < ScanElement > () . unwrap () . with_context (Some (& context))) ; + component . expect ("" . parse :: < ScanElement > () . unwrap () . with_context (Some (& context))) ; + component . expect ("\n\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t" . parse :: < ScanElement > () . unwrap () . with_context (Some (& context))) ; + component . expect ("\n\t\t\n\t" . parse :: < ScanElement > () . unwrap () . with_context (Some (& context))) ; + component . expect ("" . parse :: < ScanElement > () . unwrap () . with_context (Some (& context))) ; + component . expect ("\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t" . parse :: < ScanElement > () . unwrap () . with_context (Some (& context))) ; + component . expect ("\n\t\t\n\t\t\tThe following rooms were destroyed:\nelsewhere@conference.localhost\n\t\t\n\t" . parse :: < ScanElement > () . unwrap () . with_context (Some (& context))) ; + println!("FOO: {component:?}"); + handle_stanza(&mut component, &mut rooms).await.unwrap(); +} diff --git a/src/tests/scansion/muc_creation.rs b/src/tests/scansion/muc_creation.rs new file mode 100644 index 0000000..82d5177 --- /dev/null +++ b/src/tests/scansion/muc_creation.rs @@ -0,0 +1,18 @@ +#![doc = "MUC Creation\nSingle user MUC creation\ntag: muc\n"] +use crate::component::TestComponent; +use crate::handlers::handle_stanza; +use crate::room::Room; +use scansion::ScanElement; +use std::collections::HashMap; +use xmpp_parsers::{BareJid, Element}; +#[tokio::test] +async fn spec() { + println!("FOO0: {:?}", "louise\n"); + println!("FOO0: {:?}", "louise@localhost/194\n"); + let stanzas_in = vec ! [ScanElement :: new ("\n\t\t\n\t" . parse :: < Element > () . unwrap ()) . with_context (Some (& context))] ; + let mut component = TestComponent::new_scan(stanzas_in); + let mut rooms: HashMap = HashMap::new(); + component . expect ("\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t" . parse :: < ScanElement > () . unwrap () . with_context (Some (& context))) ; + println!("FOO: {component:?}"); + handle_stanza(&mut component, &mut rooms).await.unwrap(); +} diff --git a/src/tests/scansion/muc_mediated_invite.rs b/src/tests/scansion/muc_mediated_invite.rs new file mode 100644 index 0000000..44bc4fb --- /dev/null +++ b/src/tests/scansion/muc_mediated_invite.rs @@ -0,0 +1,12 @@ +use crate::component::TestComponent; +use crate::handlers::handle_stanza; +use crate::room::Room; +use std::collections::HashMap; +use xmpp_parsers::BareJid; +#[doc = "MUC: Mediated invites\n"] +#[tokio::test] +async fn spec() { + let mut component = TestComponent::new(vec![]); + let mut rooms: HashMap = HashMap::new(); + handle_stanza(&mut component, &mut rooms).await.unwrap(); +}