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) (#2693)
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 #1464 within the new test framework. This is related to #2234 and makes the mdbook-slide-evaluator from #2258 obsolete and should be removed as this is a not so powerful nor flexible framework.
This commit is contained in:
parent
91f6de64df
commit
53f7660e9b
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@ -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
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
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
|
||||
}
|
||||
}
|
53
tests/src/slide-size.test.ts
Normal file
53
tests/src/slide-size.test.ts
Normal file
@ -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);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
47
tests/src/slides/create-slide.list.sh
Executable file
47
tests/src/slides/create-slide.list.sh
Executable file
@ -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};
|
18
tests/src/slides/slide-exemptions.list.ts
Normal file
18
tests/src/slides/slide-exemptions.list.ts
Normal file
@ -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",
|
||||
];
|
7
tests/src/slides/slides.list.ts
Normal file
7
tests/src/slides/slides.list.ts
Normal file
@ -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 = [];
|
Loading…
x
Reference in New Issue
Block a user