Initial project synthesis
This commit is contained in:
commit
f114c9ed52
21 changed files with 3366 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
**/target
|
||||
*.log
|
||||
*.tjson
|
1585
Cargo.lock
generated
Normal file
1585
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
14
Cargo.toml
Normal file
14
Cargo.toml
Normal 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
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
17
cef-test-core/Cargo.toml
Normal 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" }
|
5
cef-test-core/src/cef/mod.rs
Normal file
5
cef-test-core/src/cef/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
//! Chrome Embedded Framework functionality
|
||||
|
||||
mod web_content;
|
||||
|
||||
pub use web_content::WebContent;
|
62
cef-test-core/src/cef/web_content.rs
Normal file
62
cef-test-core/src/cef/web_content.rs
Normal 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(), "");
|
||||
}
|
||||
}
|
||||
}
|
4
cef-test-core/src/harness/adaptor.rs
Normal file
4
cef-test-core/src/harness/adaptor.rs
Normal file
|
@ -0,0 +1,4 @@
|
|||
/// API-specific implementation of interacting with CEF DevTools
|
||||
pub trait TestAdaptor {
|
||||
//TODO
|
||||
}
|
30
cef-test-core/src/harness/feedback.rs
Normal file
30
cef-test-core/src/harness/feedback.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
66
cef-test-core/src/harness/harness.rs
Normal file
66
cef-test-core/src/harness/harness.rs
Normal 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)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
26
cef-test-core/src/harness/headless_adaptor.rs
Normal file
26
cef-test-core/src/harness/headless_adaptor.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
13
cef-test-core/src/harness/instructions.rs
Normal file
13
cef-test-core/src/harness/instructions.rs
Normal 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 {}
|
7
cef-test-core/src/harness/json_runner/mod.rs
Normal file
7
cef-test-core/src/harness/json_runner/mod.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
//! JSON Runner implementation
|
||||
mod runner;
|
||||
mod structure;
|
||||
|
||||
pub use runner::JsonRunner;
|
||||
pub use structure::Test;
|
||||
pub(super) use structure::*;
|
43
cef-test-core/src/harness/json_runner/runner.rs
Normal file
43
cef-test-core/src/harness/json_runner/runner.rs
Normal 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
|
||||
}
|
||||
}
|
35
cef-test-core/src/harness/json_runner/structure.rs
Normal file
35
cef-test-core/src/harness/json_runner/structure.rs
Normal 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 {}
|
18
cef-test-core/src/harness/mod.rs
Normal file
18
cef-test-core/src/harness/mod.rs
Normal 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};
|
46
cef-test-core/src/harness/runner.rs
Normal file
46
cef-test-core/src/harness/runner.rs
Normal 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
9
cef-test-core/src/lib.rs
Normal 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;
|
6
cef-test-core/src/util.rs
Normal file
6
cef-test-core/src/util.rs
Normal 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
24
src/cli.rs
Normal 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
26
src/main.rs
Normal 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");
|
||||
}
|
Loading…
Reference in a new issue