1
0
mirror of https://github.com/google/comprehensive-rust.git synced 2025-04-07 15:38:47 +02:00

Evaluate slide size and block if they grow above a certain treshold (with exemption mechanism) ()

This enables a test for the width and height of slides (excluding some
special cases completely).
The mechanism has an exemption mechanism to temporarily exempt slides
from the rules.
Even exempted slides are checked for the rule violation and once the
slides are compliant they must be removed from the exemption list to
avoid future regression (the check fails in the CI if compliant slides
are exempted!)

This also provides a good opportunity to always have an up-to-date list
of overlong slides in
[slide-exemptions.list.ts](tests/src/slides/slide-exemptions.list.ts)
that can be worked on.

The slide list is always autogenerated in the CI environment. If you want to
enable this for your local dev environment it has to be created manually.
This avoids a time consuming local test if it is not necessary.

On the CLI it can be locally used with `npm run test --
--spec=src/slide-size.test.ts` (after creating the list with
`./src/slides/create-slide.list.sh ../book/html/`).
The CI environment specifies the env var `TEST_BOOK_DIR` that is used to
specifiy the html directory so it can create the list of slides
on-the-fly, check against hardcoded exemptions and evaluate.

This is a new solution for  within the new test framework. This is
related to  and makes the mdbook-slide-evaluator from 
obsolete and should be removed as this is a not so powerful nor flexible
framework.
This commit is contained in:
michael-kerscher 2025-03-18 12:50:46 +01:00 committed by GitHub
parent 91f6de64df
commit 53f7660e9b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 131 additions and 818 deletions

@ -190,7 +190,9 @@ 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

3
.gitignore vendored

@ -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

210
Cargo.lock generated

@ -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"
@ -2796,28 +2665,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"
@ -2991,37 +2838,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"
@ -3259,12 +3075,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"
@ -3481,26 +3291,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"

@ -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",

@ -1,22 +0,0 @@
[package]
name = "mdbook-slide-evaluator"
version = "0.1.0"
authors = ["Michael Kerscher <kerscher@google.com>"]
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"

@ -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.

@ -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<PathBuf>,
/// 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<PolicyViolation>,
}
/// holds all evaluation results for a book
pub struct EvaluationResults {
/// metadata about the book
_book: Book,
/// the collected evaluation results
results: Vec<EvaluationResult>,
}
#[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::<Vec<_>>()
.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::<Vec<_>>()
.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<PathBuf>,
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<Option<Element>> {
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<ElementSize> {
let coordinates = Into::<ElementSize>::into(element.rectangle().await?);
Ok(coordinates)
}
/// store the screenshot as png to the given path
fn store_screenshot(
&self,
screenshot: Vec<u8>,
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<Option<EvaluationResult>> {
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<EvaluationResults> {
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<PolicyViolation> {
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<PolicyViolation> {
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<PolicyViolation> {
[self.eval_height(element_size), self.eval_width(element_size)]
.into_iter()
.flatten()
.collect()
}
}

@ -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;

@ -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<PathBuf>,
/// 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<PathBuf>,
/// 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(())
}

@ -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<Path>,
}
/// 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<Slide>,
}
impl Book {
/// create a book from all html files in the source_dir
pub fn from_html_slides(source_dir: PathBuf) -> anyhow::Result<Book> {
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
}
}

@ -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);
},
);
}
}
});

@ -0,0 +1,47 @@
#!/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 <book_html_dir>"
exit 1
fi
# check if this is the correct root directory by checking if it contains the index.html
if [[ ! -f "${TEST_BOOK_DIR}/index.html" ]]; then
echo "Could not find index.html in ${TEST_BOOK_DIR}. Please check if the correct directory is used (e.g. book/html). You might need to (re)create the directory with mdbook build."
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};

@ -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",
];

@ -0,0 +1,7 @@
// to enable local testing for slide size checks please (re)generate this file by executing:
// $ ./tests/src/slides/create-slide.list.sh book/html
//
// This file is on purpose not pre-filled in the repository to avoid
// a) manual maintenance of slide list
// b) this takes some time to test
export const slides = [];