diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cf0f393080e6..9768bde9fbd5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -190,7 +190,7 @@ jobs: working-directory: ./tests - name: Test Javascript if: matrix.language == 'en' - run: npm test + run: ./src/slides/create-slide.list.sh; npm test env: TEST_BOOK_DIR: ../book/comprehensive-rust-${{ matrix.language }}/html working-directory: ./tests diff --git a/.gitignore b/.gitignore index aa0decb77b2f..24b52ca64296 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,6 @@ crowdin.yml # Python virtualenv (for mdbook-slide-evaluator local installation) .venv/ + +# tests/ framework artifacts +tests/src/slide/slides/slides.list.ts diff --git a/Cargo.lock b/Cargo.lock index 50c48d604b30..1100cc15272e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -263,7 +263,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "027bb0d98429ae334a8698531da7077bdf906419543a35a55c2cb1b66437d767" dependencies = [ "clap_builder", - "clap_derive", ] [[package]] @@ -288,18 +287,6 @@ dependencies = [ "clap", ] -[[package]] -name = "clap_derive" -version = "4.5.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.90", -] - [[package]] name = "clap_lex" version = "0.7.4" @@ -326,27 +313,6 @@ checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" name = "control-flow-basics" version = "0.1.0" -[[package]] -name = "cookie" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" -dependencies = [ - "time", - "version_check", -] - -[[package]] -name = "cookie" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" -dependencies = [ - "percent-encoding", - "time", - "version_check", -] - [[package]] name = "core-foundation" version = "0.9.4" @@ -437,27 +403,6 @@ dependencies = [ "syn 2.0.90", ] -[[package]] -name = "csv" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" -dependencies = [ - "csv-core", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "csv-core" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70" -dependencies = [ - "memchr", -] - [[package]] name = "cxx" version = "1.0.142" @@ -542,15 +487,6 @@ dependencies = [ "cxx-build", ] -[[package]] -name = "deranged" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" -dependencies = [ - "powerfmt", -] - [[package]] name = "derive_more" version = "0.99.17" @@ -697,31 +633,6 @@ dependencies = [ "thiserror 2.0.11", ] -[[package]] -name = "fantoccini" -version = "0.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7722aeee9c2be6fa131166990295089d73d973012b758a2208b9ba51af5dd024" -dependencies = [ - "base64 0.22.0", - "cookie 0.18.1", - "futures-core", - "futures-util", - "http 1.2.0", - "http-body-util", - "hyper 1.5.2", - "hyper-tls", - "hyper-util", - "mime", - "openssl", - "serde", - "serde_json", - "time", - "tokio", - "url", - "webdriver", -] - [[package]] name = "fastrand" version = "2.3.0" @@ -924,12 +835,6 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" -[[package]] -name = "glob" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" - [[package]] name = "globset" version = "0.4.14" @@ -1049,12 +954,6 @@ dependencies = [ "http 0.2.11", ] -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - [[package]] name = "hermit-abi" version = "0.3.9" @@ -1712,24 +1611,6 @@ dependencies = [ "pulldown-cmark 0.13.0", ] -[[package]] -name = "mdbook-slide-evaluator" -version = "0.1.0" -dependencies = [ - "anyhow", - "clap", - "csv", - "fantoccini", - "glob", - "log", - "pretty_env_logger", - "serde", - "strum", - "tokio", - "tokio-util", - "url", -] - [[package]] name = "memchr" version = "2.6.4" @@ -1891,12 +1772,6 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d" -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - [[package]] name = "num-modular" version = "0.6.1" @@ -2185,12 +2060,6 @@ version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69d3587f8a9e599cc7ec2c00e331f71c4e69a5f9a4b8a6efd5b07466b9736f9a" -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - [[package]] name = "ppv-lite86" version = "0.2.17" @@ -2797,28 +2666,6 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" -[[package]] -name = "strum" -version = "0.27.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32" -dependencies = [ - "strum_macros", -] - -[[package]] -name = "strum_macros" -version = "0.27.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "rustversion", - "syn 2.0.90", -] - [[package]] name = "subtle" version = "2.6.1" @@ -2992,37 +2839,6 @@ dependencies = [ "syn 2.0.90", ] -[[package]] -name = "time" -version = "0.3.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" -dependencies = [ - "deranged", - "itoa", - "num-conv", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" - -[[package]] -name = "time-macros" -version = "0.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" -dependencies = [ - "num-conv", - "time-core", -] - [[package]] name = "tinystr" version = "0.7.6" @@ -3260,12 +3076,6 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" -[[package]] -name = "unicode-segmentation" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" - [[package]] name = "unicode-width" version = "0.1.11" @@ -3482,26 +3292,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "webdriver" -version = "0.50.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "144ab979b12d36d65065635e646549925de229954de2eb3b47459b432a42db71" -dependencies = [ - "base64 0.21.5", - "bytes", - "cookie 0.16.2", - "http 0.2.11", - "log", - "serde", - "serde_derive", - "serde_json", - "thiserror 1.0.69", - "time", - "unicode-segmentation", - "url", -] - [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index 2452ca0753bd..a5ba12707cd6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,6 @@ members = [ "mdbook-course", "mdbook-exerciser", - "mdbook-slide-evaluator", "src/android/testing", "src/bare-metal/useful-crates/allocator-example", "src/bare-metal/useful-crates/zerocopy-example", diff --git a/mdbook-slide-evaluator/Cargo.toml b/mdbook-slide-evaluator/Cargo.toml deleted file mode 100644 index d1b0d928f5a5..000000000000 --- a/mdbook-slide-evaluator/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "mdbook-slide-evaluator" -version = "0.1.0" -authors = ["Michael Kerscher "] -edition = "2021" -license = "Apache-2.0" -repository = "https://github.com/google/comprehensive-rust" -description = "A tool for evaluating mdbook slides by rendering the html pages and spot violations to the policies" - -[dependencies] -anyhow = "1.0.96" -clap = { version = "4.5.31", features = ["derive"] } -csv = "1.3.1" -fantoccini = "0.21.4" -glob = "0.3.2" -log = "0.4.26" -pretty_env_logger = "0.5.0" -serde = { version = "1.0.218", features = ["derive"] } -strum = { version = "0.27.1", features = ["derive"] } -tokio = { version = "1.43.0", features = ["full"] } -tokio-util = "0.7.13" -url = "2.5.4" diff --git a/mdbook-slide-evaluator/README.md b/mdbook-slide-evaluator/README.md deleted file mode 100644 index 43dbf80f722d..000000000000 --- a/mdbook-slide-evaluator/README.md +++ /dev/null @@ -1,65 +0,0 @@ -mdbook-slide-evaluator allows you to evaluate the rendered slides. This way one -can find if there is too much content on the slides and if sorted by size one -can focus on the worst violations first. - -# How to run - -## Start a WebDriver compatible browser - -### Alternative: Docker - -Start a -[selenium docker container](https://github.com/SeleniumHQ/docker-selenium?tab=readme-ov-file#quick-start) -and mount the book folder into the container at `/book/`: - -``` -$ docker run -d -p 4444:4444 -p 7900:7900 --volume /path/to/my/workspace/comprehensive-rust/book:/book --shm-size="2g" selenium/standalone-chromium:latest -``` - -As the tool is running with a different base directory, you can use a relative -directory e.g., `../book/`: - -``` -$ cargo run -- ../book -``` - -### Alternative: Local WebDriver browser with `webdriver-manager` - -Use [webdriver-manager](https://pypi.org/project/webdriver-manager/) to install -a e.g., a `chromedriver` onto your system with: - -``` -$ pip install selenium webdriver-manager -$ python3 -from selenium import webdriver -from selenium.webdriver.chrome.service import Service -from selenium.webdriver.chrome.options import Options -from webdriver_manager.chrome import ChromeDriverManager - -driver = webdriver.Chrome(service=Service(ChromeDriverManager().install(), port=4444)) -# end the session when you are done. -``` - -You can provide the absolute path here as the browser has the same view on the -filesystem: - -``` -$ cargo run -- /path/to/my/workspace/comprehensive-rust/book -``` - -## Run mdbook-slide-size - -If a screenshot directory is provided, the tool can also create screenshots to -evaluate this manually. The tool always recursively grabs all `*.html` files -from the given directory and processes it. - -``` -cargo run -- --screenshot-dir screenshots ../book/html/ -``` - -# Roadmap - -To avoid a `docker mount`, try to build a data uri from the given slide. This -has the challenge that this contains links to local stylesheets that have to be -included. `css_inline` can be used for that and this already works (kind of). If -someone wants to take a stab at this, feel free to contact the author. diff --git a/mdbook-slide-evaluator/src/evaluator.rs b/mdbook-slide-evaluator/src/evaluator.rs deleted file mode 100644 index 2758368d2cab..000000000000 --- a/mdbook-slide-evaluator/src/evaluator.rs +++ /dev/null @@ -1,327 +0,0 @@ -// Copyright 2024 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::fs; -use std::io::Write as _; -use std::path::{Path, PathBuf}; - -use anyhow::anyhow; -use fantoccini::elements::Element; -use fantoccini::Client; -use log::{debug, warn}; -use serde::Serialize; -use strum::Display; -use tokio_util::sync::CancellationToken; -use url::Url; - -use crate::slides::{Book, Slide}; - -/// An Evaluator is used to render a book that is a collection of slides -/// and extract information from an element on that page. It further can -/// take a screenshot of this element and store it. A webclient instance is -/// created on creation and dropped once the Evaluator is dropped. -pub struct Evaluator<'a> { - /// webclient used to render html - webclient: Client, - /// selector for the element that is scored - element_selector: fantoccini::wd::Locator<'a>, - /// store screenshot in this directory if provided - screenshot_dir: Option, - /// html base uri to the source_dir used as a prefix for each page - html_base_url: Url, - /// base directory for all processed files - source_dir: PathBuf, - /// if this token is cancelled, the process needs to end gracefully - cancellation_token: CancellationToken, - /// the policy applied to the slides - slide_policy: SlidePolicy, -} - -/// element coordinates returned by the browser -#[derive(Debug)] -struct ElementSize { - /// the width of the element - width: f64, - /// the height of the element - height: f64, -} - -impl From<(f64, f64, f64, f64)> for ElementSize { - fn from(value: (f64, f64, f64, f64)) -> Self { - Self { width: value.2, height: value.3 } - } -} - -#[derive(Debug)] -/// holds the evaluation result for a slide -pub struct EvaluationResult { - /// metadata about the slide - slide: Slide, - /// the size of the main content element - element_size: ElementSize, - /// all policy violations - policy_violations: Vec, -} - -/// holds all evaluation results for a book -pub struct EvaluationResults { - /// metadata about the book - _book: Book, - /// the collected evaluation results - results: Vec, -} - -#[derive(Serialize)] -struct ExportFormat { - filename: PathBuf, - element_width: usize, - element_height: usize, - policy_violations: String, -} - -impl EvaluationResults { - /// export the evaluation results to the given csv file, overwrites if - /// allowed - pub fn export_csv( - &self, - file: &Path, - overwrite: bool, - violations_only: bool, - ) -> anyhow::Result<()> { - if file.exists() && !overwrite { - Err(anyhow!( - "Not allowed to overwrite existing evaluation results at {}", - file.display() - ))?; - }; - - let mut csv_writer = csv::Writer::from_path(file)?; - for result in &self.results { - if violations_only && result.policy_violations.is_empty() { - continue; - } - csv_writer.serialize(ExportFormat { - filename: (*result.slide.filename).to_path_buf(), - element_width: result.element_size.width.round() as usize, - element_height: result.element_size.height.round() as usize, - policy_violations: result - .policy_violations - .iter() - .map(PolicyViolation::to_string) - .collect::>() - .join(";"), - })?; - } - Ok(()) - } - - /// dump the results to stdout - pub fn export_stdout(&self, violations_only: bool) { - for result in &self.results { - if violations_only && result.policy_violations.is_empty() { - continue; - } - println!( - "{}: {}x{} [{}]", - result.slide.filename.display(), - result.element_size.width, - result.element_size.height, - result - .policy_violations - .iter() - .map(PolicyViolation::to_string) - .collect::>() - .join(";"), - ); - } - } -} - -impl<'a> Evaluator<'_> { - /// create a new instance with the provided config. - /// fails if the webclient cannot be created - pub fn new( - webclient: Client, - element_selector: &'a str, - screenshot_dir: Option, - html_base_url: Url, - source_dir: PathBuf, - cancellation_token: CancellationToken, - slide_policy: SlidePolicy, - ) -> Evaluator<'a> { - let element_selector = fantoccini::Locator::XPath(element_selector); - Evaluator { - webclient, - element_selector, - screenshot_dir, - html_base_url, - source_dir, - cancellation_token, - slide_policy, - } - } - - /// navigate the webdriver to the given url. - /// ensure that html_base_url is set before calling this - /// after this call the webdriver will see the content at the url - async fn webdriver_open_url(&self, url: &Url) -> Result<(), anyhow::Error> { - debug!("open url in webclient: {}", url); - self.webclient.goto(url.as_str()).await?; - Ok(()) - } - - /// evaluate the currently opened webpage return the selected content - /// element if available - async fn get_content_element_from_slide( - &self, - ) -> anyhow::Result> { - match self.webclient.find(self.element_selector).await { - Result::Ok(result) => Ok(Some(result)), - Result::Err(fantoccini::error::CmdError::Standard( - fantoccini::error::WebDriver { - error: fantoccini::error::ErrorStatus::NoSuchElement, - .. - }, - )) => anyhow::Ok(None), - Result::Err(error) => Err(anyhow!(error))?, - } - } - - /// extract the element coordinates from this element - async fn get_element_coordinates( - &self, - element: &Element, - ) -> anyhow::Result { - let coordinates = Into::::into(element.rectangle().await?); - Ok(coordinates) - } - - /// store the screenshot as png to the given path - fn store_screenshot( - &self, - screenshot: Vec, - filename: &Path, - ) -> anyhow::Result<()> { - let relative_filename = filename.strip_prefix(&self.source_dir)?; - let output_filename = self - .screenshot_dir - .as_ref() - .unwrap() - .join(relative_filename.with_extension("png")); - debug!("write screenshot to {}", output_filename.to_str().unwrap()); - - // create directories if necessary - let output_dir = output_filename.parent().unwrap(); - if !output_dir.exists() { - debug!("creating {}", output_dir.to_str().unwrap()); - fs::create_dir_all(output_dir)?; - } - - let mut file = - fs::OpenOptions::new().create(true).write(true).open(output_filename)?; - - file.write_all(&screenshot)?; - Ok(()) - } - - /// evaluate a single slide - pub async fn eval_slide( - &self, - slide: &Slide, - ) -> anyhow::Result> { - debug!("evaluating {:?}", slide); - - let url = self.html_base_url.join(&slide.filename.display().to_string())?; - self.webdriver_open_url(&url).await?; - - let Some(content_element) = self.get_content_element_from_slide().await? - else { - return Ok(None); - }; - let element_size = self.get_element_coordinates(&content_element).await?; - if self.screenshot_dir.is_some() { - let screenshot = content_element.screenshot().await?; - self.store_screenshot(screenshot, &slide.filename)?; - } - let policy_violations = self.slide_policy.eval_size(&element_size); - let result = EvaluationResult { - slide: slide.clone(), - element_size, - policy_violations, - }; - debug!("information about element: {:?}", result); - Ok(Some(result)) - } - - /// evaluate an entire book - pub async fn eval_book(&self, book: Book) -> anyhow::Result { - let mut results = vec![]; - debug!("slide count: {}", book.slides().len()); - for slide in book.slides().iter() { - if self.cancellation_token.is_cancelled() { - debug!("received cancel request, return already completed results"); - break; - } - let Some(result) = self.eval_slide(slide).await? else { - warn!("slide with no content - ignore: {:?}", slide); - continue; - }; - results.push(result); - } - Ok(EvaluationResults { _book: book, results }) - } -} - -/// all possible policy violations -#[derive(Debug, Display, Serialize)] -enum PolicyViolation { - /// violation of the maximum height - MaxWidth, - /// violation of the maximum width - MaxHeight, -} - -/// the SlidePolicy struct contains all parameters for evaluating a slide -pub struct SlidePolicy { - /// the maximum allowed width of a slide - pub max_width: usize, - /// the maximum allowed height of a slide - pub max_height: usize, -} - -impl SlidePolicy { - /// evaluate if the width is within the policy - fn eval_width(&self, element_size: &ElementSize) -> Option { - if element_size.width as usize > self.max_width { - return Some(PolicyViolation::MaxWidth); - } - return None; - } - - /// evaluate if the width is within the policy - fn eval_height(&self, element_size: &ElementSize) -> Option { - if element_size.height as usize > self.max_height { - return Some(PolicyViolation::MaxHeight); - } - return None; - } - - /// evaluate all size policies - fn eval_size(&self, element_size: &ElementSize) -> Vec { - [self.eval_height(element_size), self.eval_width(element_size)] - .into_iter() - .flatten() - .collect() - } -} diff --git a/mdbook-slide-evaluator/src/lib.rs b/mdbook-slide-evaluator/src/lib.rs deleted file mode 100644 index 0238c32c7ffe..000000000000 --- a/mdbook-slide-evaluator/src/lib.rs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2024 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pub mod evaluator; -pub mod slides; diff --git a/mdbook-slide-evaluator/src/main.rs b/mdbook-slide-evaluator/src/main.rs deleted file mode 100644 index b364275e3fd4..000000000000 --- a/mdbook-slide-evaluator/src/main.rs +++ /dev/null @@ -1,122 +0,0 @@ -// Copyright 2024 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::path::PathBuf; - -use clap::Parser; -use log::{debug, info}; -use mdbook_slide_evaluator::evaluator::{Evaluator, SlidePolicy}; -use mdbook_slide_evaluator::slides::Book; -use tokio_util::sync::CancellationToken; -use url::Url; - -#[derive(Parser)] -#[command(version, about, arg_required_else_help(true))] -struct Args { - /// the URI of the webdriver - #[arg(long, default_value_t=String::from("http://localhost:4444"))] - webdriver: String, - /// the XPath to element that is evaluated - #[arg(long, default_value_t=String::from(r#"//*[@id="content"]/main"#))] - element: String, - /// take screenshots of the content element if provided - #[arg(short, long)] - screenshot_dir: Option, - /// a base url that is used to render the files (relative to source_dir). - /// if you mount the slides at source_dir into / in a webdriver docker - /// container you can use the default - #[arg(long, default_value_t=Url::parse("file:///").unwrap())] - base_url: Url, - /// exports to csv file if provided, otherwise to stdout - #[arg(long)] - export: Option, - /// allows overwriting the export file - #[arg(long, default_value_t = false)] - overwrite: bool, - /// the height of the webclient that renders the slide - #[arg(long, default_value_t = 1920)] - webclient_width: u32, - /// the width of the webclient that renders the slide - #[arg(long, default_value_t = 1080)] - webclient_height: u32, - /// max width of a slide - #[arg(long, default_value_t = 750)] - width: usize, - /// max height of a slide - default height/width values have 16/9 ratio - #[arg(long, default_value_t = 1333)] - height: usize, - /// if set only violating slides are shown - #[arg(long, default_value_t = false)] - violations_only: bool, - /// directory of the book that is evaluated - source_dir: PathBuf, -} - -#[tokio::main] -async fn main() -> anyhow::Result<()> { - // pretty env receives log level from RUST_LOG env variable - pretty_env_logger::init(); - - let args = Args::parse(); - - // gather information about the book from the filesystem - let book = Book::from_html_slides(args.source_dir.clone())?; - - // create a new webclient that is used by the evaluator - let webclient: fantoccini::Client = - fantoccini::ClientBuilder::native().connect(&args.webdriver).await?; - // use a defined window size for reproducible results - webclient.set_window_size(args.webclient_width, args.webclient_height).await?; - - let cancellation_token = CancellationToken::new(); - - let slide_policy = - SlidePolicy { max_width: args.width, max_height: args.height }; - - // create a new evaluator (connects to the provided webdriver) - let evaluator = Evaluator::new( - webclient.clone(), - &args.element, - args.screenshot_dir, - args.base_url, - args.source_dir.to_path_buf(), - cancellation_token.clone(), - slide_policy, - ); - - tokio::spawn(async move { - tokio::signal::ctrl_c().await.unwrap(); - info!("received CTRL+C"); - // send a cancel signal - cancellation_token.cancel(); - }); - - // evaluate each slide - let score_results = evaluator.eval_book(book).await?; - - if let Some(export_file) = args.export { - score_results.export_csv( - &export_file, - args.overwrite, - args.violations_only, - )?; - } else { - score_results.export_stdout(args.violations_only); - } - - // close webclient as otherwise the unclosed session cannot be reused - debug!("closing webclient"); - webclient.close().await?; - Ok(()) -} diff --git a/mdbook-slide-evaluator/src/slides.rs b/mdbook-slide-evaluator/src/slides.rs deleted file mode 100644 index f97e6070382b..000000000000 --- a/mdbook-slide-evaluator/src/slides.rs +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright 2024 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::path::{Path, PathBuf}; -use std::sync::Arc; - -use log::debug; - -/// a slide is a page in the book -#[derive(Debug, Clone)] -pub struct Slide { - pub filename: Arc, -} - -/// a book is a collection of slides -pub struct Book { - /// the path to the root directory of this book - _source_dir: PathBuf, - /// the collection of slides - slides: Vec, -} - -impl Book { - /// create a book from all html files in the source_dir - pub fn from_html_slides(source_dir: PathBuf) -> anyhow::Result { - let mut slides = vec![]; - let files = glob::glob(&format!( - "{}/**/*.html", - source_dir.to_str().expect("invalid path") - ))?; - for file in files { - let slide = Slide { filename: file?.into() }; - debug!("add {:?}", slide); - slides.push(slide); - } - Ok(Book { _source_dir: source_dir, slides }) - } - - /// return a reference to the slides of this book - pub fn slides(&self) -> &[Slide] { - &self.slides - } -} diff --git a/tests/src/slide-size.test.ts b/tests/src/slide-size.test.ts new file mode 100644 index 000000000000..e5cf198ec30e --- /dev/null +++ b/tests/src/slide-size.test.ts @@ -0,0 +1,53 @@ +import { describe, it } from "mocha"; +import { $, expect, browser } from "@wdio/globals"; +import { slides } from "./slides/slides.list"; +import { exemptions } from "./slides/slide-exemptions.list"; + +// these are empirically determined values in 16:9 ratio +const MAX_HEIGHT = 1333; +const MAX_WIDTH = 750; + +describe("Slide", () => { + for (const slide of slides) { + if (exemptions.includes(slide)) { + // This slide is exempted and violated rules before. + // It is expected to still do this and if not it should be removed from exemptions. + // This acts as a regression check + it( + " " + + slide + + " is on the exemption list but should be removed from slide-exemptions.list.ts", + async () => { + await browser.url("/" + slide); + const main_element = $("#content > main"); + const main_element_size = await main_element.getSize(); + console.info("slide " + slide + " is on the exemption list"); + // one of them (height, width) should fail + expect( + main_element_size.height >= MAX_HEIGHT || + main_element_size.width > MAX_WIDTH, + ).toBe(true); + }, + ); + } else { + it( + " " + + slide + + " should not be higher than " + + MAX_HEIGHT + + " pixels or wider than " + + MAX_WIDTH + + " pixels", + async () => { + await browser.url("/" + slide); + const main_element = $("#content > main"); + const main_element_size = await main_element.getSize(); + expect( + main_element_size.height < MAX_HEIGHT && + main_element_size.width <= MAX_WIDTH, + ).toBe(true); + }, + ); + } + } +}); diff --git a/tests/src/slides/create-slide.list.sh b/tests/src/slides/create-slide.list.sh new file mode 100755 index 000000000000..10b77158ae43 --- /dev/null +++ b/tests/src/slides/create-slide.list.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +# This script (re)creates the slides.list.ts file based on the given book html directory. +# It is used to regenerate the list of slides that are tested in the slide-size.test.ts file. +# Takes either TEST_BOOK_DIR environment variable or first parameter as override. + +set -e +BASEDIR="$(dirname "$0")" + +if [[ -n "$1" ]]; then + # take directory from command line + TEST_BOOK_DIR="$1" +fi + +# check if TEST_BOOK_DIR is empty (not set by environment nor parameter) +if [[ -z "${TEST_BOOK_DIR}" ]]; then + echo "Usage: $0 " + exit 1 +fi + +pushd "${TEST_BOOK_DIR}" +# exclude special pages that should never be tested +SLIDES=$(grep -L "Redirecting to..." -R --include "*.html" \ + --exclude "exercise.html" \ + --exclude "solution.html" \ + --exclude "toc.html" \ + --exclude "print.html" \ + --exclude "404.html" \ + --exclude "glossary.html" \ + --exclude "index.html" \ + --exclude "course-structure.html" + ) +popd +OUTPUT="${BASEDIR}/slides.list.ts" + +# create a ts module that can be imported in the tests +echo "export const slides = [" > ${OUTPUT}; +for SLIDE in ${SLIDES}; do +echo " \"${SLIDE}\"," >> ${OUTPUT}; +done; +echo "];" >> ${OUTPUT}; diff --git a/tests/src/slides/slide-exemptions.list.ts b/tests/src/slides/slide-exemptions.list.ts new file mode 100644 index 000000000000..d868ff411623 --- /dev/null +++ b/tests/src/slides/slide-exemptions.list.ts @@ -0,0 +1,18 @@ +// These slides are known to violate the slide style guide. +// They are checked if they still violate and if not fail the test. +// Please remove slides that become good so they don't regress. +export const exemptions = [ + "android/interoperability/java.html", + "android/testing.html", + "bare-metal/aps/entry-point.html", + "exercises/bare-metal/compass.html", + "exercises/bare-metal/solutions-afternoon.html", + "exercises/bare-metal/rtc.html", + "exercises/bare-metal/solutions-morning.html", + "exercises/chromium/interoperability-with-cpp.html", + "exercises/chromium/bringing-it-together.html", + "concurrency/async-exercises/chat-app.html", + "concurrency/async-exercises/solutions.html", + "concurrency/sync-exercises/solutions.html", + "concurrency/sync-exercises/link-checker.html", +];