From 7486b0c9679d145a76813811ed52d9fd12e03ca0 Mon Sep 17 00:00:00 2001 From: bokuweb Date: Sun, 17 Mar 2024 18:46:39 +0900 Subject: [PATCH] init --- .gitignore | 155 +++++++++++++++++++++++++++++++++ Cargo.toml | 5 ++ crates/core/Cargo.toml | 18 ++++ crates/core/examples/simple.rs | 5 ++ crates/core/src/lib.rs | 144 ++++++++++++++++++++++++++++++ crates/core/src/main.rs | 5 ++ crates/core/src/report.rs | 111 +++++++++++++++++++++++ package.json | 2 +- 8 files changed, 444 insertions(+), 1 deletion(-) create mode 100644 Cargo.toml create mode 100644 crates/core/Cargo.toml create mode 100644 crates/core/examples/simple.rs create mode 100644 crates/core/src/lib.rs create mode 100644 crates/core/src/main.rs create mode 100644 crates/core/src/report.rs diff --git a/.gitignore b/.gitignore index 2ad53959..d17bcb23 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,158 @@ report.html /screenshot/* sample/index.html from-json.html +### Generated by gibo (https://github.com/simonwhitaker/gibo) +### https://raw.github.com/github/gitignore/e5323759e387ba347a9d50f8b0ddd16502eb71d4/Node.gitignore + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + + +### https://raw.github.com/github/gitignore/e5323759e387ba347a9d50f8b0ddd16502eb71d4/Rust.gitignore + +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +wasi-sdk-21.0 +m.md + diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..657b566a --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,5 @@ +[workspace] +resolver = "2" +members = [ + "crates/*", +] \ No newline at end of file diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml new file mode 100644 index 00000000..569b929f --- /dev/null +++ b/crates/core/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "core" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +image-diff-rs = { git = "https://github.com/bokuweb/image-diff-rs.git" } +globmatch = "0.3" +rayon = "1.8" +mustache = "0.9.0" +serde = { version = "1.0.207", features = ["derive"] } +serde_json = "1.0.125" +percent-encoding = "2.3.1" + +[dev-dependencies] +rstest = "0.18.2" diff --git a/crates/core/examples/simple.rs b/crates/core/examples/simple.rs new file mode 100644 index 00000000..365085c0 --- /dev/null +++ b/crates/core/examples/simple.rs @@ -0,0 +1,5 @@ +use core::run; + +pub fn main() { + run("./sample/actual/", "./sample/expected") +} diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs new file mode 100644 index 00000000..176f4d09 --- /dev/null +++ b/crates/core/src/lib.rs @@ -0,0 +1,144 @@ +// type RegParams = { +// actualDir: string, +// expectedDir: string, +// diffDir: string, +// report?: string, +// junitReport?: string, +// json?: string, +// update?: boolean, +// extendedErrors?: boolean, +// urlPrefix?: string, +// matchingThreshold?: number, +// threshold?: number, // alias to thresholdRate. +// thresholdRate?: number, +// thresholdPixel?: number, +// concurrency?: number, +// enableAntialias?: boolean, +// enableClientAdditionalDetection?: boolean, +// }; +// fn main() { +// // let a = include_bytes!("../../sample/actual/sample.png"); +// // let b = include_bytes!("../../sample/expected/sample.png"); +// // image_diff_rs::diff( +// // a, +// // b, +// // &image_diff_rs::DiffOption { +// // threshold: Some(0.1), +// // include_anti_alias: Some(true), +// // }, +// // ) +// // .unwrap(); +// // crate::run(); +// } +// +//const aggregate = result => { +// const passed = result.filter(r => r.passed).map(r => r.image); +// const failed = result.filter(r => !r.passed).map(r => r.image); +// const diffItems = failed.map(image => image.replace(/\.[^\.]+$/, '.png')); +// return { passed, failed, diffItems }; +// }; +mod report; + +use image_diff_rs::DiffOption; +use rayon::prelude::*; +use std::{ + collections::BTreeSet, + path::{Path, PathBuf}, +}; + +static IMAGE_FILES: &str = "/**/*.{tiff,jpeg,jpg,gif,png,bmp,webp}"; + +#[derive(Debug)] +pub(crate) struct DetectedImages { + pub(crate) expected: BTreeSet, + pub(crate) actual: BTreeSet, + pub(crate) deleted: BTreeSet, + pub(crate) new: BTreeSet, +} + +pub fn run(expected_dir: impl AsRef, actual_dir: impl AsRef) { + let actual_dir = actual_dir.as_ref().to_owned(); + let expected_dir = expected_dir.as_ref().to_owned(); + let detected = find_images(&expected_dir, &actual_dir); + + let targets: Vec = detected + .actual + .intersection(&detected.expected) + .cloned() + .collect(); + + let result: Result, std::io::Error> = targets + .par_iter() + .map(|path| { + let img1 = std::fs::read(actual_dir.clone().join(path))?; + let img2 = std::fs::read(expected_dir.clone().join(path))?; + let res = image_diff_rs::diff( + img1, + img2, + &DiffOption { + threshold: Some(0.05), + include_anti_alias: Some(true), + }, + ); + std::fs::write("./test.png", res.unwrap().diff_image)?; + Ok(()) + }) + .inspect(|r| if let Err(e) = r { /*TODO: logging */ }) + .collect(); + report::Report::create(); +} + +pub(crate) fn find_images( + expected_dir: impl AsRef, + actual_dir: impl AsRef, +) -> DetectedImages { + let expected_dir = expected_dir.as_ref(); + let actual_dir = actual_dir.as_ref(); + + let expected: BTreeSet = + globmatch::Builder::new(&(expected_dir.display().to_string() + IMAGE_FILES)) + .build(".") + .expect("the pattern should be correct.") + .into_iter() + .flatten() + .map(|p| p.strip_prefix(expected_dir).unwrap().to_path_buf()) + .collect(); + + let actual: BTreeSet = + globmatch::Builder::new(&(actual_dir.display().to_string() + IMAGE_FILES)) + .build(".") + .expect("the pattern should be correct.") + .into_iter() + .flatten() + .map(|p| p.strip_prefix(actual_dir).unwrap().to_path_buf()) + .collect(); + + let deleted = expected.difference(&actual).cloned().collect(); + + let new = actual.difference(&expected).cloned().collect(); + + DetectedImages { + expected, + actual, + deleted, + new, + } +} + +fn is_passed( + width: u32, + height: u32, + diff_count: u32, + threshold_pixel: Option, + threshold_rate: Option, +) -> bool { + if let Some(t) = threshold_pixel { + diff_count <= t + } else if let Some(t) = threshold_rate { + let pixel = width * height; + let ratio = diff_count as f32 / pixel as f32; + ratio <= t + } else { + diff_count == 0 + } +} diff --git a/crates/core/src/main.rs b/crates/core/src/main.rs new file mode 100644 index 00000000..365085c0 --- /dev/null +++ b/crates/core/src/main.rs @@ -0,0 +1,5 @@ +use core::run; + +pub fn main() { + run("./sample/actual/", "./sample/expected") +} diff --git a/crates/core/src/report.rs b/crates/core/src/report.rs new file mode 100644 index 00000000..31676f9f --- /dev/null +++ b/crates/core/src/report.rs @@ -0,0 +1,111 @@ +use mustache::MapBuilder; +use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC}; +use serde::Serialize; + +pub(crate) struct Report; + +pub(crate) enum ReportStatus { + Success, + Fail, +} + +pub(crate) struct ReportInput<'a> { + // passed: &'a [&'a str], + // failed_items: [&'a str], + // new_items: [&'a str], + // deleted_items: [&'a str], + // expected_items: [&'a str], + // actual_items: [&'a str], + differences: [&'a str], + // json: string, + // actualDir: string, + // expectedDir: string, + // diffDir: string, + // report: string, + // junitReport: string, + // extendedErrors: boolean, + // urlPrefix: string, + // enableClientAdditionalDetection: boolean, + // fromJSON?: boolean, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct ReportItem { + pub(crate) raw: String, + pub(crate) encoded: String, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct XimgdiffConfig { + pub(crate) enabled: bool, + pub(crate) worker_url: String, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct ReportJson { + r#type: String, // 'success' : 'danger', + has_new: bool, + new_items: Vec, + has_deleted: bool, + deleted_items: Vec, + has_passed: bool, + passed_items: Vec, + has_failed: bool, + failed_items: Vec, + actual_dir: String, + expected_dir: String, + diff_dir: String, + ximgdiff_config: XimgdiffConfig, +} + +fn encode_file_path(file_path: &str) -> String { + file_path + .split(std::path::MAIN_SEPARATOR) + .map(|p| utf8_percent_encode(p, NON_ALPHANUMERIC).to_string()) + .collect::>() + .join(&std::path::MAIN_SEPARATOR.to_string()) +} + +impl Report { + pub fn create() { + let template = include_str!("../../../template/template.html"); + let js = include_str!("../../../report/ui/dist/report.js"); + // const view = { + // js, + // report: JSON.stringify(json), + // faviconData: loadFaviconAsDataURL(faviconType), + // }; + let json = ReportJson { + r#type: "success".to_string(), + has_new: false, + new_items: vec![], + has_deleted: false, + deleted_items: vec![], + has_passed: false, + passed_items: vec![], + has_failed: true, + failed_items: vec![], + actual_dir: "".to_string(), + expected_dir: "".to_string(), + diff_dir: "".to_string(), + ximgdiff_config: XimgdiffConfig { + enabled: false, + worker_url: "".to_string(), + }, + }; + let data = MapBuilder::new() + .insert_str("js", js) + .insert_str( + "report", + serde_json::to_string(&json).expect("should convert."), + ) + .build(); + let template = mustache::compile_str(template).expect("should compile template."); + template + .render_data(&mut std::io::stdout(), &data) + .expect("should render report."); + } +} diff --git a/package.json b/package.json index 1767bbc2..a3b8b567 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "flow": "flow", "copy:ximgdiff": "copyfiles -u 3 node_modules/x-img-diff-js/build/cv-wasm_browser.* report/assets", "prepublishOnly": "npm run build", - "reg": "node dist/cli.js ./sample/actual ./sample/expected ./sample/diff -I -R ./sample/index.html -T 0.01 -X client", + "reg": "node dist/cli.js ./sample/actual ./sample/expected ./sample/diff -I -R ./sample/index.html -T 0.01", "reg:from": "node dist/cli.js -F ./sample/reg.json -R ./sample/index.html", "screenshot": "node test/screenshot.js", "test:cli": "chmod +x dist/cli.js && ava test/cli.test.mjs",