1use std::fs;
2use std::path::{Path, PathBuf};
3use std::process::Command;
4
5use failure::*;
6use fantoccini::{Client, Locator};
7use futures::{future::Future, stream::Stream, sync::oneshot::channel};
8use json;
9use log::*;
10use proc_macro2::{Span, TokenStream};
11use quote::quote;
12use syn::Ident;
13
14fn main() {
15 env_logger::init();
16 // this requires being run by cargo, which is iffy
17 let root_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
18 let repo_root = root_dir.parent().and_then(Path::parent).unwrap();
19
20 let fixtures_root = repo_root.join("test_fixtures");
21 let fixtures = fs::read_dir(fixtures_root).unwrap();
22
23 info!("reading test fixtures from disk");
24 let mut fixtures: Vec<_> = fixtures
25 .into_iter()
26 .filter_map(|a| a.ok())
27 .filter(|f| f.path().is_file() && f.path().extension().map(|p| p == "html").unwrap_or(false))
28 .map(|f| {
29 let fixture_path = f.path().to_path_buf();
30 let name = fixture_path.file_stem().unwrap().to_str().unwrap().to_string();
31 (name, fixture_path)
32 })
33 .collect();
34 fixtures.sort_unstable_by_key(|f| f.0.clone());
35
36 info!("starting webdriver instance");
37 let webdriver_url = "http://localhost:4444";
38 let mut webdriver_handle = Command::new("chromedriver").arg("--port=4444").spawn().unwrap();
39
40 // this is silly, but it works
41 std::thread::sleep(std::time::Duration::from_secs(1));
42
43 let mut caps = serde_json::map::Map::new();
44 let chrome_opts = serde_json::json!({ "args": ["--headless", "--disable-gpu"] });
45 caps.insert("goog:chromeOptions".to_string(), chrome_opts.clone());
46
47 use indicatif::ProgressBar;
48 let pb = ProgressBar::new(fixtures.len() as u64);
49 let (test_descs_sink, mut test_descs) = channel();
50
51 info!("spawning webdriver client and collecting test descriptions");
52 tokio::run(
53 Client::with_capabilities(webdriver_url, caps.clone())
54 .map_err(|e| Error::from(e))
55 .and_then(move |client| {
56 futures::stream::iter_ok(pb.wrap_iter(fixtures.into_iter()))
57 .and_then(move |(name, fixture_path)| {
58 pb.set_message(&name);
59 test_root_element(client.clone(), name, fixture_path)
60 })
61 .collect()
62 .map(move |descs| {
63 info!("finished collecting descriptions, sending them back to main");
64 let _ = test_descs_sink.send(descs);
65 })
66 })
67 .map_err(|e| {
68 error!("top-level error encountered: {:?}", e);
69 }),
70 );
71
72 info!("killing webdriver instance...");
73 webdriver_handle.kill().unwrap();
74
75 info!("collecting test descriptions from async runtime...");
76 let test_descs = loop {
77 if let Ok(Some(descs)) = test_descs.try_recv() {
78 break descs;
79 }
80 };
81
82 info!("generating test sources and concatenating...");
83
84 let bench_descs: Vec<_> = test_descs
85 .iter()
86 .map(|(name, description)| {
87 debug!("generating bench contents for {}", &name);
88 (name.clone(), generate_bench(description))
89 })
90 .collect();
91
92 let test_descs: Vec<_> = test_descs
93 .iter()
94 .map(|(name, description)| {
95 debug!("generating test contents for {}", &name);
96 (name.clone(), generate_test(name, description))
97 })
98 .collect();
99
100 let benchmarks: Vec<_> = test_descs
101 .iter()
102 .map(|(name, _)| {
103 let bench_mod = Ident::new(name, Span::call_site());
104 quote!(#bench_mod::compute())
105 })
106 .collect();
107
108 let test_mods = test_descs
109 .iter()
110 .map(|(name, _)| {
111 let name = Ident::new(name, Span::call_site());
112 quote!(mod #name;)
113 })
114 .fold(quote!(), |a, b| quote!(#a #b));
115
116 for (name, bench_body) in bench_descs {
117 let mut bench_filename = repo_root.join("benches").join("generated").join(&name);
118 bench_filename.set_extension("rs");
119 debug!("writing {} to disk...", &name);
120 fs::write(bench_filename, bench_body.to_string()).unwrap();
121 }
122
123 for (name, test_body) in test_descs {
124 let mut test_filename = repo_root.join("tests").join("generated").join(&name);
125 test_filename.set_extension("rs");
126 debug!("writing {} to disk...", &name);
127 fs::write(test_filename, test_body.to_string()).unwrap();
128 }
129
130 let bench_mods = quote!(
131 use criterion::{criterion_group, criterion_main, Criterion};
132
133 #test_mods
134
135 fn benchmark(c: &mut Criterion) {
136 c.bench_function("generated benchmarks", |b| {
137 b.iter(|| { #(#benchmarks;)* })
138 });
139 }
140
141 criterion_group!(benches, benchmark);
142 criterion_main!(benches);
143 );
144
145 info!("writing generated test file to disk...");
146 fs::write(repo_root.join("benches").join("generated").join("mod.rs"), bench_mods.to_string()).unwrap();
147 fs::write(repo_root.join("tests").join("generated").join("mod.rs"), test_mods.to_string()).unwrap();
148
149 info!("formatting the source directory");
150 Command::new("cargo").arg("fmt").current_dir(repo_root).status().unwrap();
151}
152
153fn test_root_element(
154 client: Client,
155 name: String,
156 fixture_path: impl AsRef<Path>,
157) -> impl Future<Item = (String, json::JsonValue), Error = Error> {
158 let fixture_path = fixture_path.as_ref();
159
160 let url = format!("file://{}", fixture_path.display());
161 let nav_client = client.clone();
162 let mut locate_client = client.clone();
163
164 nav_client
165 .goto(&url)
166 .map_err(|e| e.context("navigating to file"))
167 .and_then(move |_| {
168 locate_client.find(Locator::Css("#test-root")).map_err(|e| e.context("finding #test-root")).and_then(
169 |mut root| {
170 root.attr("__stretch_description__")
171 .map_err(|e| e.context("retrieving stretch description from test root"))
172 .and_then(|description_string| {
173 json::parse(&description_string.unwrap())
174 .map(|d| (name, d))
175 .context("parsing test description")
176 })
177 },
178 )
179 })
180 .map_err(|c| c.into())
181}
182
183fn generate_bench(description: &json::JsonValue) -> TokenStream {
184 let node_description = generate_node("node", &description);
185
186 quote!(
187 pub fn compute() {
188 let mut stretch = taffy::Stretch::new();
189 #node_description
190 stretch.compute_layout(node, taffy::geometry::Size::undefined()).unwrap();
191 }
192 )
193}
194
195fn generate_test(name: impl AsRef<str>, description: &json::JsonValue) -> TokenStream {
196 let name = name.as_ref();
197 let name = Ident::new(name, Span::call_site());
198 let node_description = generate_node("node", &description);
199 let assertions = generate_assertions("node", &description);
200
201 quote!(
202 #[test]
203 fn #name() {
204 let mut stretch = taffy::Stretch::new();
205 #node_description
206 stretch.compute_layout(node, taffy::geometry::Size::undefined()).unwrap();
207 #assertions
208 }
209 )
210}
211
212fn generate_assertions(ident: &str, node: &json::JsonValue) -> TokenStream {
213 let layout = &node["layout"];
214
215 let read_f32 = |s: &str| layout[s].as_f32().unwrap();
216 let width = read_f32("width");
217 let height = read_f32("height");
218 let x = read_f32("x");
219 let y = read_f32("y");
220
221 let children = {
222 let mut c = Vec::new();
223 match node["children"] {
224 json::JsonValue::Array(ref value) => {
225 for i in 0..value.len() {
226 let child = &value[i];
227 c.push(generate_assertions(&format!("{}{}", ident, i), child));
228 }
229 }
230 _ => (),
231 };
232 c.into_iter().fold(quote!(), |a, b| quote!(#a #b))
233 };
234
235 let ident = Ident::new(ident, Span::call_site());
236
237 quote!(
238 assert_eq!(stretch.layout(#ident).unwrap().size.width, #width);
239 assert_eq!(stretch.layout(#ident).unwrap().size.height, #height);
240 assert_eq!(stretch.layout(#ident).unwrap().location.x, #x);
241 assert_eq!(stretch.layout(#ident).unwrap().location.y, #y);
242
243 #children
244 )
245}
246
247fn generate_node(ident: &str, node: &json::JsonValue) -> TokenStream {
248 let style = &node["style"];
249
250 let display = match style["display"] {
251 json::JsonValue::Short(ref value) => match value.as_ref() {
252 "none" => quote!(display: taffy::style::Display::None,),
253 _ => quote!(),
254 },
255 _ => quote!(),
256 };
257
258 let position_type = match style["position_type"] {
259 json::JsonValue::Short(ref value) => match value.as_ref() {
260 "absolute" => quote!(position_type: taffy::style::PositionType::Absolute,),
261 _ => quote!(),
262 },
263 _ => quote!(),
264 };
265
266 let direction = match style["direction"] {
267 json::JsonValue::Short(ref value) => match value.as_ref() {
268 "rtl" => quote!(direction: taffy::style::Direction::RTL,),
269 "ltr" => quote!(direction: taffy::style::Direction::LTR,),
270 _ => quote!(),
271 },
272 _ => quote!(),
273 };
274
275 let flex_direction = match style["flexDirection"] {
276 json::JsonValue::Short(ref value) => match value.as_ref() {
277 "row-reverse" => quote!(flex_direction: taffy::style::FlexDirection::RowReverse,),
278 "column" => quote!(flex_direction: taffy::style::FlexDirection::Column,),
279 "column-reverse" => quote!(flex_direction: taffy::style::FlexDirection::ColumnReverse,),
280 _ => quote!(),
281 },
282 _ => quote!(),
283 };
284
285 let flex_wrap = match style["flexWrap"] {
286 json::JsonValue::Short(ref value) => match value.as_ref() {
287 "wrap" => quote!(flex_wrap: taffy::style::FlexWrap::Wrap,),
288 "wrap-reverse" => quote!(flex_wrap: taffy::style::FlexWrap::WrapReverse,),
289 _ => quote!(),
290 },
291 _ => quote!(),
292 };
293
294 let overflow = match style["overflow"] {
295 json::JsonValue::Short(ref value) => match value.as_ref() {
296 "hidden" => quote!(overflow: taffy::style::Overflow::Hidden,),
297 "scroll" => quote!(overflow: taffy::style::Overflow::Scroll,),
298 _ => quote!(),
299 },
300 _ => quote!(),
301 };
302
303 let align_items = match style["alignItems"] {
304 json::JsonValue::Short(ref value) => match value.as_ref() {
305 "flex-start" => quote!(align_items: taffy::style::AlignItems::FlexStart,),
306 "flex-end" => quote!(align_items: taffy::style::AlignItems::FlexEnd,),
307 "center" => quote!(align_items: taffy::style::AlignItems::Center,),
308 "baseline" => quote!(align_items: taffy::style::AlignItems::Baseline,),
309 _ => quote!(),
310 },
311 _ => quote!(),
312 };
313
314 let align_self = match style["alignSelf"] {
315 json::JsonValue::Short(ref value) => match value.as_ref() {
316 "flex-start" => quote!(align_self: taffy::style::AlignSelf::FlexStart,),
317 "flex-end" => quote!(align_self: taffy::style::AlignSelf::FlexEnd,),
318 "center" => quote!(align_self: taffy::style::AlignSelf::Center,),
319 "baseline" => quote!(align_self: taffy::style::AlignSelf::Baseline,),
320 "stretch" => quote!(align_self: taffy::style::AlignSelf::Stretch,),
321 _ => quote!(),
322 },
323 _ => quote!(),
324 };
325
326 let align_content = match style["alignContent"] {
327 json::JsonValue::Short(ref value) => match value.as_ref() {
328 "flex-start" => quote!(align_content: taffy::style::AlignContent::FlexStart,),
329 "flex-end" => quote!(align_content: taffy::style::AlignContent::FlexEnd,),
330 "center" => quote!(align_content: taffy::style::AlignContent::Center,),
331 "space-between" => quote!(align_content: taffy::style::AlignContent::SpaceBetween,),
332 "space-around" => quote!(align_content: taffy::style::AlignContent::SpaceAround,),
333 _ => quote!(),
334 },
335 _ => quote!(),
336 };
337
338 let justify_content = match style["justifyContent"] {
339 json::JsonValue::Short(ref value) => match value.as_ref() {
340 "flex-end" => quote!(justify_content: taffy::style::JustifyContent::FlexEnd,),
341 "center" => quote!(justify_content: taffy::style::JustifyContent::Center,),
342 "space-between" => quote!(justify_content: taffy::style::JustifyContent::SpaceBetween,),
343 "space-around" => quote!(justify_content: taffy::style::JustifyContent::SpaceAround,),
344 "space-evenly" => quote!(justify_content: taffy::style::JustifyContent::SpaceEvenly,),
345 _ => quote!(),
346 },
347 _ => quote!(),
348 };
349
350 let flex_grow = match style["flexGrow"] {
351 json::JsonValue::Number(value) => {
352 let value: f32 = value.into();
353 quote!(flex_grow: #value,)
354 }
355 _ => quote!(),
356 };
357
358 let flex_shrink = match style["flexShrink"] {
359 json::JsonValue::Number(value) => {
360 let value: f32 = value.into();
361 quote!(flex_shrink: #value,)
362 }
363 _ => quote!(),
364 };
365
366 let flex_basis = match style["flexBasis"] {
367 json::JsonValue::Object(ref value) => {
368 let value = generate_dimension(value);
369 quote!(flex_basis: #value,)
370 }
371 _ => quote!(),
372 };
373
374 let size = match style["size"] {
375 json::JsonValue::Object(ref value) => {
376 let size = generate_size(value);
377 quote!(size: #size,)
378 }
379 _ => quote!(),
380 };
381
382 let min_size = match style["min_size"] {
383 json::JsonValue::Object(ref value) => {
384 let min_size = generate_size(value);
385 quote!(min_size: #min_size,)
386 }
387 _ => quote!(),
388 };
389
390 let max_size = match style["max_size"] {
391 json::JsonValue::Object(ref value) => {
392 let max_size = generate_size(value);
393 quote!(max_size: #max_size,)
394 }
395 _ => quote!(),
396 };
397
398 macro_rules! edges_quoted {
399 ($style:ident, $val:ident) => {
400 let $val = match $style[stringify!($val)] {
401 json::JsonValue::Object(ref value) => {
402 let edges = generate_edges(value);
403 quote!($val: #edges,)
404 },
405 _ => quote!(),
406 };
407 };
408 }
409
410 edges_quoted!(style, margin);
411 edges_quoted!(style, padding);
412 edges_quoted!(style, position);
413 edges_quoted!(style, border);
414
415 let (children_body, children) = match node["children"] {
416 json::JsonValue::Array(ref value) => {
417 if value.len() > 0 {
418 let body = value
419 .iter()
420 .enumerate()
421 .map(|(i, child)| generate_node(&format!("{}{}", ident, i), child))
422 .collect();
423 let idents = value
424 .iter()
425 .enumerate()
426 .map(|(i, _)| Ident::new(&format!("{}{}", ident, i), Span::call_site()))
427 .collect::<Vec<_>>();
428 (body, quote!(vec![#(#idents),*]))
429 } else {
430 (quote!(), quote!(vec![]))
431 }
432 }
433 _ => (quote!(), quote!()),
434 };
435
436 let ident = Ident::new(&format!("{}", ident), Span::call_site());
437
438 quote!(
439 #children_body
440 let #ident = stretch.new_node(
441 taffy::style::Style {
442 #display
443 #direction
444 #position_type
445 #flex_direction
446 #flex_wrap
447 #overflow
448 #align_items
449 #align_self
450 #align_content
451 #justify_content
452 #flex_grow
453 #flex_shrink
454 #flex_basis
455 #size
456 #min_size
457 #max_size
458 #margin
459 #padding
460 #position
461 #border
462 ..Default::default()
463 },
464 #children
465 ).unwrap();)
466}
467
468macro_rules! dim_quoted {
469 ($obj:ident, $dim_name:ident) => {
470 let $dim_name = match $obj.get(stringify!($dim_name)) {
471 Some(json::JsonValue::Object(ref value)) => {
472 let dim = generate_dimension(value);
473 quote!($dim_name: #dim,)
474 }
475 _ => quote!(),
476 };
477 };
478}
479
480fn generate_size(size: &json::object::Object) -> TokenStream {
481 dim_quoted!(size, width);
482 dim_quoted!(size, height);
483 quote!(
484 taffy::geometry::Size {
485 #width #height
486 ..Default::default()
487 }
488 )
489}
490
491fn generate_dimension(dimen: &json::object::Object) -> TokenStream {
492 let unit = dimen.get("unit").unwrap();
493 let value = || dimen.get("value").unwrap().as_f32().unwrap();
494
495 match unit {
496 json::JsonValue::Short(ref unit) => match unit.as_ref() {
497 "auto" => quote!(taffy::style::Dimension::Auto),
498 "points" => {
499 let value = value();
500 quote!(taffy::style::Dimension::Points(#value))
501 }
502 "percent" => {
503 let value = value();
504 quote!(taffy::style::Dimension::Percent(#value))
505 }
506 _ => unreachable!(),
507 },
508 _ => unreachable!(),
509 }
510}
511
512fn generate_edges(dimen: &json::object::Object) -> TokenStream {
513 dim_quoted!(dimen, start);
514 dim_quoted!(dimen, end);
515 dim_quoted!(dimen, top);
516 dim_quoted!(dimen, bottom);
517
518 quote!(taffy::geometry::Rect {
519 #start #end #top #bottom
520 ..Default::default()
521 })
522}