| // Copyright 2023 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| use anyhow::{Error, Result}; |
| use chrono::Datelike; |
| use handlebars::{handlebars_helper, Handlebars}; |
| use std::path::{Path, PathBuf}; |
| use tempfile::TempDir; |
| use tracing::info; |
| use walkdir::{DirEntry, WalkDir}; |
| |
| /// Creates a `TemplateFile` for a handlebars template. |
| /// |
| /// $file is the path to the handlebars template file relative to |
| /// testgen's templates/ directory. A leading $dir path can be provided |
| /// to strip that prefix from the output filename. Arguments should not |
| /// contain leading or trailing slashes. |
| /// |
| /// # Examples |
| /// |
| /// ``` |
| /// // Reads from 'template/realm_factory/src/main.rs.hbrs' and |
| /// // chooses the output file 'realm_factory/src/main.rs' |
| /// hbrs_template_file!("realm_factory/src/main.rs") |
| /// |
| /// // Reads from 'template/realm_factory/src/main.rs.hbrs' and |
| /// // chooses the output file 'src/main.rs' |
| /// hbrs_template_file!("realm_factory", "src/main.rs") |
| /// ``` |
| macro_rules! hbrs_template_file { |
| ($file:expr) => { |
| TemplateFile { |
| filename: $file, |
| contents: include_str!(concat!("../templates/", $file, ".hbrs")), |
| } |
| }; |
| ($dir:expr, $file:expr) => { |
| TemplateFile { |
| filename: $file, |
| contents: include_str!(concat!("../templates/", $dir, "/", $file, ".hbrs")), |
| } |
| }; |
| } |
| // Make this template available throughout the crate. |
| pub(crate) use hbrs_template_file; |
| |
| /// A template file for code generation. |
| /// filename is the output filename where the template code will be generated. |
| /// contents is the template string. |
| /// Do not construct directly, use `hbrs_template_file` instead. |
| pub(crate) struct TemplateFile { |
| pub filename: &'static str, |
| pub contents: &'static str, |
| } |
| |
| /// Returns the value of the template variable `component_exposed_protocols`. |
| pub(crate) fn var_component_exposed_protocols(component: &cml::Document) -> Vec<String> { |
| list_exposed_protocol_names(component) |
| } |
| |
| /// Returns the value of the template variable `rel_component_url`. |
| pub(crate) fn var_rel_component_url(component_name: &str) -> String { |
| format!("#meta/{}.cm", component_name) |
| } |
| |
| /// Returns the value of the template variable 'realm_factory_binary_name'. |
| pub(crate) fn var_realm_factory_binary_name(component_name: &str) -> String { |
| let binary_name = format!("{}_realm_factory", component_name); |
| sanitize_rust_binary_name(&binary_name) |
| } |
| |
| /// Returns the value of the template variable 'test_binary_name'. |
| pub(crate) fn var_test_binary_name(component_name: &str) -> String { |
| let binary_name = format!("{}_test", component_name); |
| sanitize_rust_binary_name(&binary_name) |
| } |
| |
| /// Returns the value of the template variable 'test_package_name'. |
| pub(crate) fn var_test_package_name(component_name: &str) -> String { |
| format!("{}-test", component_name) |
| } |
| |
| /// Returns the value of the template variable 'fidl_library_name'. |
| pub(crate) fn var_fidl_library_name(component_name: &str) -> String { |
| let library_name = format!("test.{}", component_name); |
| let library_name = library_name.replace("_", ""); |
| let library_name = library_name.replace("-", ""); |
| library_name |
| } |
| |
| /// Returns the value of the template variable 'fidl_rust_crate_name'. |
| pub(crate) fn var_fidl_rust_crate_name(component_name: &str) -> String { |
| let crate_name = var_fidl_library_name(component_name); |
| let crate_name = format!("fidl_{}", crate_name); |
| let crate_name = crate_name.replace(".", "_"); |
| let crate_name = crate_name.replace("-", "_"); |
| crate_name |
| } |
| |
| /// Replaces characters in `binary_name` with the same replacements |
| /// the Fuchsia build uses for rust binary names. |
| pub(crate) fn sanitize_rust_binary_name(binary_name: &str) -> String { |
| let binary_name = String::from(binary_name); |
| let binary_name = binary_name.replace("-", "_"); |
| binary_name |
| } |
| |
| /// Creates a directory at `path` if `path` it does not exist. |
| pub(crate) fn dir_create_if_absent<P: AsRef<Path>>(path: P) -> Result<(), Error> { |
| let path = path.as_ref(); |
| if !path.exists() { |
| info!("Creating directory {}", path.display()); |
| std::fs::create_dir_all(path)?; |
| } |
| Ok(()) |
| } |
| |
| /// Recursively copies the directory at `src` to `dst`. |
| pub(crate) fn dir_copy(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Result<(), Error> { |
| let src = src.as_ref(); |
| let dst = dst.as_ref(); |
| let skip_root = true; |
| for entry in walk_dir(src, skip_root) { |
| let entry_src = entry.path(); |
| let entry_dst = dst.join(path_relative_to(src, entry_src)); |
| if entry.file_type().is_dir() { |
| dir_copy(entry_src, entry_dst)?; |
| } else { |
| file_copy(entry_src, entry_dst)?; |
| } |
| } |
| Ok(()) |
| } |
| |
| /// Copies the file at `src` to `dst`, creating any parent directories if they do not exist. |
| pub(crate) fn file_copy(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Result<(), Error> { |
| let src = src.as_ref(); |
| let dst = dst.as_ref(); |
| dir_create_if_absent(dst.parent().unwrap())?; |
| info!("Copying file {} to {}", src.display(), dst.display()); |
| std::fs::copy(src, dst)?; |
| Ok(()) |
| } |
| |
| /// Writes `contents` to `path`, creating any parent directories if they do not exist. |
| pub(crate) fn file_write<P: AsRef<Path>>(path: P, contents: &str) -> Result<(), Error> { |
| let path = path.as_ref(); |
| dir_create_if_absent(path.parent().unwrap())?; |
| info!("Writing file {}", path.display()); |
| std::fs::write(path, contents)?; |
| Ok(()) |
| } |
| |
| /// Parses a .cml file as a [`cml::Document`]. |
| pub(crate) fn load_cml_file<P: AsRef<Path>>(path: P) -> Result<cml::Document> { |
| let path = path.as_ref(); |
| let contents = std::fs::read_to_string(path)?; |
| let document = cml::parse_one_document(&contents, path)?; |
| Ok(document) |
| } |
| |
| /// Returns a vector of the protocol capability names exposed by `component`. |
| pub(crate) fn list_exposed_protocol_names(component: &cml::Document) -> Vec<String> { |
| match &component.expose { |
| None => return vec![], |
| Some(exposes) => { |
| exposes.iter().fold(vec![], |mut protocols, expose| match &expose.protocol { |
| None => protocols, |
| Some(cml::OneOrMany::One(name)) => { |
| protocols.push(name.as_str().to_string()); |
| protocols |
| } |
| Some(cml::OneOrMany::Many(names)) => { |
| for name in names.iter() { |
| protocols.push(name.as_str().to_string()); |
| } |
| protocols |
| } |
| }) |
| } |
| } |
| } |
| |
| /// Returns the stem of the path `path`. |
| /// |
| /// The stem is the non-extension portion of the basename. |
| /// |
| /// Examples: |
| /// |
| /// path_file_stem("/a/b/stem.c") => "stem" |
| /// path_file_stem("/a/b/stem.c.gz") => "stem.c" |
| /// path_file_stem("/a/b/") => "b" |
| /// path_file_stem("/a/b//") => "b" |
| /// |
| /// # Panics |
| /// |
| /// This panics if the given path has no stem, which can only occur if path is the empty string. |
| pub(crate) fn path_file_stem<P: AsRef<Path>>(path: P) -> String { |
| path.as_ref().file_stem().unwrap().to_str().unwrap().to_string() |
| } |
| |
| /// Returns a path that, when appended to `base` yields `path`. |
| pub(crate) fn path_relative_to(base: impl AsRef<Path>, path: impl AsRef<Path>) -> PathBuf { |
| path.as_ref().strip_prefix(base).unwrap().to_owned() |
| } |
| |
| // Walks the directory at `path` skipping '.' and '..'. |
| // If `skip_root` is true the returned iterator skips `path` and only iterates over its children. |
| pub(crate) fn walk_dir<P: AsRef<Path>>(path: P, skip_root: bool) -> impl Iterator<Item = DirEntry> { |
| let mut walk = WalkDir::new(path); |
| if skip_root { |
| walk = walk.min_depth(1); |
| } |
| walk.into_iter().filter_entry(is_not_hidden).filter_map(|v| v.ok()) |
| } |
| |
| // Returns true if entry is a hidden filesystem entity. |
| pub(crate) fn is_not_hidden(entry: &DirEntry) -> bool { |
| entry.file_name().to_str().map(|s| entry.depth() == 0 || !s.starts_with(".")).unwrap_or(false) |
| } |
| |
| /// All variables available to handlebars templates. |
| /// |
| /// All of the variables here are available to all templates, but |
| /// different testgen subcommands may use default values for any |
| /// subset of these variables. |
| #[derive(Default, serde::Serialize)] |
| pub(crate) struct TemplateVars { |
| /// The name of the component, derived from the name of the input component manifest. |
| /// Example: /path/to/my-component.cml -> my-component |
| pub component_name: String, |
| |
| /// The list of protocols exposed by the component under test, if any. |
| pub component_exposed_protocols: Vec<String>, |
| |
| /// The absolute GN target label of the component under test. |
| pub component_gn_label: String, |
| |
| /// The package-relative URL of the component under test. This is only valid |
| /// within the generated realm factory package. |
| pub rel_component_url: String, |
| |
| /// The name of the generated test binary. |
| pub test_binary_name: String, |
| |
| /// The name of the generated test Fuchsia package. |
| /// Example GN usage: `fuchsia_test_package("{{ test_package_name }}")` |
| pub test_package_name: String, |
| |
| /// The name of the generated test realm factory binary name. |
| pub realm_factory_binary_name: String, |
| |
| /// The name of the Rust crate containing the test's generated RealmFactory FIDL bindings. |
| /// This is primarily useful for importing generated Rust fidl bindings in test code. |
| /// Example Rust usage: `use {{ fidl_rust_crate_name }} as ftest` |
| pub fidl_rust_crate_name: String, |
| |
| /// The name of the RealmFactory fidl library. This is primarily useful for defining and importing |
| /// the FIDL library in .fidl files. |
| /// Example FIDL usage: `library {{ fidl_library_name }};` |
| pub fidl_library_name: String, |
| } |
| |
| impl TemplateVars { |
| pub(crate) fn new() -> Self { |
| Self { ..Default::default() } |
| } |
| } |
| |
| /// Generates a directory tree by executing handlebars templates against a set of variables. |
| pub(crate) struct CodeGenerator { |
| handlebars: handlebars::Handlebars<'static>, |
| template_files: Vec<TemplateFile>, |
| template_vars: TemplateVars, |
| } |
| |
| impl CodeGenerator { |
| pub(crate) fn new() -> Self { |
| let mut code_generator = Self { |
| handlebars: Handlebars::new(), |
| template_files: vec![], |
| template_vars: TemplateVars::new(), |
| }; |
| code_generator.install_handlebars_helpers(); |
| code_generator |
| } |
| |
| /// Adds a `TemplateFile` that will be generated into an ouptut file when |
| /// [`CodeGenerator::generate`] is called. |
| /// |
| /// To create the TemplateFile, use [`hbrs_template_file`]. |
| pub(crate) fn with_template(mut self, template_file: TemplateFile) -> Self { |
| self.template_files.push(template_file); |
| self |
| } |
| |
| /// Sets the template variables to use when generating code. |
| /// |
| /// The variables will be available for use by all templates when code is generated. |
| pub(crate) fn with_template_vars(mut self, template_vars: TemplateVars) -> Self { |
| self.template_vars = template_vars; |
| self |
| } |
| |
| /// Generates code and writes all files and directories to disk. |
| pub(crate) fn generate<P: AsRef<Path>>(self, path: P) -> Result<(), Error> { |
| // Generate code in a staging directory in case of failure. |
| let tmp_dir = TempDir::new()?; |
| for TemplateFile { filename, contents } in self.template_files { |
| let code = self.handlebars.render_template(&contents, &self.template_vars)?; |
| let output_path = tmp_dir.path().join(filename); |
| file_write(output_path, &code)?; |
| } |
| |
| // Commit changes. |
| dir_create_if_absent(&path)?; |
| dir_copy(tmp_dir.path(), &path)?; |
| Ok(()) |
| } |
| |
| fn install_handlebars_helpers(&mut self) { |
| // Returns the current year. |
| handlebars_helper!(helper_year: |*args| { |
| let _ = args; |
| format!("{}", chrono::Utc::now().year()) |
| }); |
| |
| self.handlebars.register_helper("year", Box::new(helper_year)); |
| self.handlebars.set_strict_mode(true); |
| } |
| } |