Initial project synthesis

This commit is contained in:
NGnius (Graham) 2023-01-14 17:37:01 -05:00
commit f114c9ed52
21 changed files with 3366 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
**/target
*.log
*.tjson

1585
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

14
Cargo.toml Normal file
View file

@ -0,0 +1,14 @@
[package]
name = "cef-test-cli"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
cef-test-core = { version = "0.1.0", path = "./cef-test-core" }
clap = { version = "4", features = [ "derive" ] }
# logging
log = "0.4"
simplelog = "0.12"

1327
cef-test-core/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

17
cef-test-core/Cargo.toml Normal file
View file

@ -0,0 +1,17 @@
[package]
name = "cef-test-core"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
chrono = { version = "0.4" }
log = "0.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
ureq = { version = "2.6", features = [ "json" ] }
# adaptor
headless_chrome = { version = "1.0" }

View file

@ -0,0 +1,5 @@
//! Chrome Embedded Framework functionality
mod web_content;
pub use web_content::WebContent;

View file

@ -0,0 +1,62 @@
use serde::{Deserialize, Serialize};
/// WebContent json information retrieved from Chrome DevTools at `http://<IP>:<PORT>/json`
#[derive(Serialize, Deserialize, Debug)]
pub struct WebContent {
description: String,
#[serde(rename = "devtoolsFrontendUrl")]
devtools_frontend_url: String,
id: String,
title: String,
url: String,
#[serde(rename = "webSocketDebuggerUrl")]
web_socket_debugger_url: String,
}
impl WebContent {
/// Get id
pub fn id(&self) -> &str {
&self.id
}
/// Get title
pub fn title(&self) -> &str {
&self.title
}
/// Get url
pub fn url(&self) -> &str {
&self.url
}
/// Get websocket debugger url
pub fn debug_url(&self) -> &str {
&self.web_socket_debugger_url
}
/// Retrieve WebContent information from CEF instance
#[allow(clippy::result_large_err)]
pub fn load_all(domain_name: &str, port: u16) -> Result<Vec<Self>, ureq::Error> {
Ok(ureq::get(&format!("http://{}:{}/json", domain_name, port))
.call()?
.into_json()?)
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn retrieve_web_content() {
let contents = WebContent::load_all(env!("DECK_IP"), 8081).expect("Unable to retrieve inspectable web contents json");
assert_ne!(contents.len(), 0, "No web contents found!");
for c in contents {
println!("{:?}", c);
assert_ne!(c.id(), "");
assert_ne!(c.title(), "");
assert_ne!(c.url(), "");
assert_ne!(c.debug_url(), "");
}
}
}

View file

@ -0,0 +1,4 @@
/// API-specific implementation of interacting with CEF DevTools
pub trait TestAdaptor {
//TODO
}

View file

@ -0,0 +1,30 @@
/// Harness information for a test runner
pub enum Feedback {
/// Start of run (no feedback to provide)
Start,
/// Last instruction was successful
Success,
/// Last instruction was an assertion and it failed
AssertFailure,
/// Last instruction raised an error
Error,
}
impl Feedback {
/// Feedback is indicative of regular operations
pub fn is_ok(&self) -> bool {
match self {
Self::Success => true,
Self::Start => true,
_ => false,
}
}
/// Feedback is indicative of an error
pub fn is_err(&self) -> bool {
match self {
Self::Error => true,
_ => false,
}
}
}

View file

@ -0,0 +1,66 @@
use super::{TestRunner, TestAdaptor, TestMetadata};
use super::{Instruction, TestAssert, UIOp, Feedback};
/// Harness which runs one or more tests
pub struct TestHarness<R: TestRunner, A: TestAdaptor> {
tests: Vec<R>,
adaptor: A,
}
impl<R: TestRunner, A: TestAdaptor> TestHarness<R, A> {
/// Construct a new test harness
pub fn new(adaptor: A, tests: Vec<R>) -> Self {
Self {
adaptor,
tests,
}
}
fn translate_assertion(&self, _assertion: TestAssert) -> Feedback {
// TODO
Feedback::Success
}
fn translate_ui_op(&self, _op: UIOp) -> Feedback {
// TODO
Feedback::Success
}
fn translate_instruction(&self, instruction: Instruction) -> Feedback {
match instruction {
Instruction::Assertion(a) => self.translate_assertion(a),
Instruction::Interaction(i) => self.translate_ui_op(i),
}
}
/// Perform the tests
pub fn execute(mut self) -> Result<A, Vec<TestMetadata>> {
// TODO
let tests: Vec<R> = self.tests.drain(..).collect();
let mut failures = Vec::with_capacity(tests.len());
for mut test in tests {
let mut feedback = Feedback::Start;
let mut is_success = true;
let metadata = test.meta();
log::info!("Starting test {}: {}", metadata.id, metadata.name);
while let Some(instruction) = test.next(feedback) {
feedback = self.translate_instruction(instruction);
is_success &= feedback.is_ok();
}
let mut metadata = test.meta();
metadata.success &= is_success;
if metadata.success {
log::info!("{}", metadata);
} else {
log::error!("{}", metadata);
failures.push(metadata);
}
}
if failures.is_empty() {
Ok(self.adaptor)
} else {
Err(failures)
}
}
}

View file

@ -0,0 +1,26 @@
use std::collections::HashMap;
use headless_chrome::Browser;
use crate::cef::WebContent;
/// Headless Chrome Adaptor for CEF
pub struct HeadlessAdaptor {
web_content: Vec<WebContent>,
connections: HashMap<String, Browser>,
domain_name: String,
port_num: u16
}
impl HeadlessAdaptor {
/// Connect DevTools and prepare to connect to the browser
#[allow(clippy::result_large_err)]
pub fn connect(domain_name: &str, port: u16) -> Result<Self, ureq::Error> {
Ok(Self {
web_content: WebContent::load_all(domain_name, port)?,
connections: HashMap::new(),
domain_name: domain_name.to_owned(),
port_num: port,
})
}
}

View file

@ -0,0 +1,13 @@
/// Instruction for the text harness to perform
pub enum Instruction {
/// Test assertion
Assertion(TestAssert),
/// UI manipulation
Interaction(UIOp),
}
/// Assertion
pub enum TestAssert {}
/// User interface interaction
pub enum UIOp {}

View file

@ -0,0 +1,7 @@
//! JSON Runner implementation
mod runner;
mod structure;
pub use runner::JsonRunner;
pub use structure::Test;
pub(super) use structure::*;

View file

@ -0,0 +1,43 @@
use super::super::{Instruction, Feedback, TestRunner, TestMetadata};
use super::Test;
/// Test runner for specific JSON data structures.
pub struct JsonRunner {
test_data: Test,
success: bool,
}
impl JsonRunner {
/// Load test information from file
pub fn from_file<P: AsRef<std::path::Path>>(path: P) -> std::io::Result<Self> {
let file = std::io::BufReader::new(std::fs::File::open(path.as_ref())?);
let test = serde_json::from_reader(file)?;
Ok(Self {
test_data: test,
success: true,
})
}
/// Construct JsonRunner in memory
pub fn new(test: Test) -> Self {
Self {
test_data: test,
success: true,
}
}
}
impl TestRunner for JsonRunner {
fn next(&mut self, feedback: Feedback) -> Option<Instruction> {
// TODO
self.success = feedback.is_ok();
log::error!("JsonRunner.next(...) is UNIMPLEMENTED!");
None
}
fn meta(&self) -> TestMetadata {
let mut metadata: TestMetadata = self.test_data.info.clone().into();
metadata.success = self.success;
metadata
}
}

View file

@ -0,0 +1,35 @@
use serde::{Deserialize, Serialize};
use super::super::TestMetadata;
/// Test descriptor
#[derive(Serialize, Deserialize, Debug)]
pub struct Test {
pub(super) info: TestInfo,
pub(super) test: Vec<TestInstruction>,
}
/// Test metadata
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct TestInfo {
pub name: String,
pub blame: String,
pub id: String,
pub output: String,
}
impl std::convert::From<TestInfo> for TestMetadata {
fn from(other: TestInfo) -> Self {
TestMetadata {
name: other.name,
id: other.id,
output: Some(other.output.into()),
author: Some(other.blame),
success: true,
}
}
}
/// Test metadata
#[derive(Serialize, Deserialize, Debug)]
pub struct TestInstruction {}

View file

@ -0,0 +1,18 @@
//! Test execution functionality
mod adaptor;
mod feedback;
#[allow(clippy::module_inception)]
mod harness;
mod headless_adaptor;
mod instructions;
mod json_runner;
mod runner;
pub use adaptor::TestAdaptor;
pub use feedback::Feedback;
pub use harness::TestHarness;
pub use headless_adaptor::HeadlessAdaptor;
pub use instructions::{Instruction, TestAssert, UIOp};
pub use json_runner::JsonRunner;
pub use runner::{TestRunner, TestMetadata};

View file

@ -0,0 +1,46 @@
/// Test runner invoked by the test harness.
/// A lot like std::iter::Iterator but which accepts input information.
pub trait TestRunner: Send + Sync {
/// Perform next action
fn next(&mut self, feedback: super::Feedback) -> Option<super::Instruction>;
/// Get test information
fn meta(&self) -> TestMetadata;
}
/// Information about the test and the run
#[derive(Default, Clone, Debug)]
pub struct TestMetadata {
/// Test name
pub name: String,
/// Test ID
pub id: String,
/// Test dump file
pub output: Option<std::path::PathBuf>,
/// Test author
pub author: Option<String>,
/// Was the test successful, or (if incomplete) is it currently passing?
pub success: bool,
}
impl std::fmt::Display for TestMetadata {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "TEST {}: {}", self.id, self.name)?;
if let Some(author) = &self.author {
write!(f, " by {}", author)?;
}
if self.success {
write!(f, " SUCCESS")?;
} else {
write!(f, " FAILURE")?;
}
if let Some(output) = &self.output {
write!(f, " ({})", output.display())?;
}
Ok(())
}
}

9
cef-test-core/src/lib.rs Normal file
View file

@ -0,0 +1,9 @@
//! Core library used for running automated tests on a Chrome Embedded Framework instance with Chrome DevTools enabled
//!
//! Very WIP right now
#![warn(missing_docs)]
#![allow(clippy::match_like_matches_macro)]
pub mod cef;
pub mod harness;
pub mod util;

View file

@ -0,0 +1,6 @@
//! Miscellaneous utility functionality
/// Get the timestamp of right now in the local timezone
pub fn timestamp_now() -> String {
chrono::offset::Local::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, false)
}

24
src/cli.rs Normal file
View file

@ -0,0 +1,24 @@
use std::path::PathBuf;
use clap::Parser;
/// -WIP- Automated test tool for CEF UIs
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
pub struct Cli {
/// CEF DevTools port
#[arg(short, long)]
port: Option<u16>,
/// CEF DevTools IP address or domain
#[arg(short, long)]
address: Option<String>,
/// Test file(s)
test: Vec<PathBuf>,
}
impl Cli {
pub fn parse() -> Self {
Parser::parse()
}
}

26
src/main.rs Normal file
View file

@ -0,0 +1,26 @@
mod cli;
use simplelog::{LevelFilter, WriteLogger};
const PACKAGE_NAME: &'static str = env!("CARGO_PKG_NAME");
const PACKAGE_VERSION: &'static str = env!("CARGO_PKG_VERSION");
fn main() {
let args = cli::Cli::parse();
println!("Got args {:?}", &args);
let log_filepath = format!("./{}-{}-v{}.log", cef_test_core::util::timestamp_now(), PACKAGE_NAME, PACKAGE_VERSION);
WriteLogger::init(
#[cfg(debug_assertions)]
{
LevelFilter::Debug
},
#[cfg(not(debug_assertions))]
{
LevelFilter::Info
},
Default::default(),
std::fs::File::create(&log_filepath).unwrap(),
).expect("Couldn't init file log");
}