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