<commit_before>use std::{
    collections::HashMap,
    ffi::OsString,
    fs::{create_dir_all, read_to_string},
    path::{Path, PathBuf},
};

use binary_node::{BinaryInstallation, BinaryTrait, NodeBinary};
use buildpack::{
    eyre,
    fs_utils::{copy_if_exists, move_dir_all, symlink_dir, symlink_file},
    hash_utils::str_sha256_hex,
    is_cnb_build, is_local_build,
    libcnb::{
        self,
        build::{BuildContext, BuildResult, BuildResultBuilder},
        data::{build_plan::BuildPlan, layer_content_metadata::LayerTypes, layer_name},
        detect::{DetectContext, DetectResult, DetectResultBuilder},
        generic::{GenericMetadata, GenericPlatform},
        layer::{ExistingLayerStrategy, Layer, LayerResult, LayerResultBuilder},
        layer_env::{LayerEnv, ModificationBehavior, Scope},
        Buildpack,
    },
    tracing, BuildpackTrait, LayerHashMetadata, LayerVersionMetadata, SYSTEM_INSTALLED,
};

pub struct NodeBuildpack;

impl BuildpackTrait for NodeBuildpack {
    fn toml() -> &'static str {
        include_str!("../buildpack.toml")
    }
}

const NODE_MODULES: &str = "node_modules";
const NVMRC: &str = ".nvmrc";
const PACKAGE_JSON: &str = "package.json";
const PACKAGE_LOCK: &str = "package-lock.json";
const TOOL_VERSIONS: &str = ".tool-versions";

impl Buildpack for NodeBuildpack {
    type Platform = GenericPlatform;
    type Metadata = GenericMetadata;
    type Error = eyre::Report;

    fn detect(&self, _context: DetectContext<Self>) -> libcnb::Result<DetectResult, Self::Error> {
        // Read `.tool-versions` for Node.js version
        let tool_versions = Self::tool_versions();

        // Read `.nvmrc` for Node.js version
        let nvmrc = read_to_string(NVMRC)
            .map(|content| content.trim().to_string())
            .ok();

        // Read `package.json` for Node.js version
        let package_json = read_to_string(PACKAGE_JSON)
            .ok()
            .and_then(|json| serde_json::from_str::<serde_json::Value>(&json).ok());

        // Detect `package-lock.json`
        let package_lock = PathBuf::from(PACKAGE_LOCK);

        // Fail early
        if !(tool_versions.contains_key("nodejs")
            || tool_versions.contains_key("node")
            || package_json.is_some()
            || package_lock.exists()
            || nvmrc.is_some()
            || Self::any_exist(&["main.js", "index.js"]))
        {
            return DetectResultBuilder::fail().build();
        }

        let mut requires = Vec::new();
        let mut provides = Vec::new();

        // Resolve Node.js version from `.tool-versions`, `.nvmrc`, `package.json`, or installed `node` version
        let (version, source) = if let Some(version) = tool_versions
            .get("nodejs")
            .or_else(|| tool_versions.get("node"))
        {
            (version.to_string(), TOOL_VERSIONS)
        } else if let Some(versionish) = nvmrc {
            (versionish, NVMRC)
        } else if let Some(semver) = package_json.as_ref().and_then(|package| {
            package
                .pointer("/engines/node")
                .and_then(|semver| semver.as_str().map(|semver| semver.to_string()))
        }) {
            (semver, PACKAGE_JSON)
        } else if let Some(version) = (NodeBinary {}).installed_version(None) {
            (version, SYSTEM_INSTALLED)
        } else {
            ("".to_string(), "")
        };

        // Require and provide Node.js
        let (require, provide) = Self::require_and_provide(
            format!("node {}", version).trim(),
            source,
            format!("Install Node.js {}", version).trim(),
        );
        requires.push(require);
        provides.push(provide);

        // Determine how NPM packages are to be installed
        if package_lock.exists() || package_json.is_some() {
            let (require, provide) = Self::require_and_provide(
                "node_modules",
                if package_lock.exists() {
                    PACKAGE_LOCK
                } else {
                    PACKAGE_JSON
                },
                "Install Node.js packages into `node_modules`",
            );
            requires.push(require);
            provides.push(provide);
        }

        let mut build_plan = BuildPlan::new();
        build_plan.requires = requires;
        build_plan.provides = provides;
        DetectResultBuilder::pass().build_plan(build_plan).build()
    }

    fn build(&self, context: BuildContext<Self>) -> libcnb::Result<BuildResult, Self::Error> {
        let entries: HashMap<_, _> = context
            .buildpack_plan
            .entries
            .iter()
            .map(|entry| Self::split_entry_name(&entry.name))
            .collect();

        if let Some(args) = entries.get("node") {
            context.handle_layer(layer_name!("node"), NodeLayer::new(args.clone()))?;
        }

        if entries.contains_key("node_modules") {
            context.handle_layer(layer_name!("node_modules"), NodeModulesLayer)?;
        }

        BuildResultBuilder::new().build()
    }
}

struct NodeLayer {
    /// The semantic version requirement for the `node` binary
    requirement: String,
}

impl NodeLayer {
    fn new(args: Vec<String>) -> Self {
        // Join args with commas because semver requirement parser expects that is
        // how parts of a requirement are separated
        let versionish = args.join(",");

        // Determine the semver requirement from versionish which could be a well formed semantic version (e.g 14.0.1),
        // a semver requirement (e.g. ^14.0), or an alias (e.g. `lts`)
        let requirement = if let Some(version) = versionish.strip_prefix('v') {
            version.to_string()
        } else if versionish == "lts" {
            // TODO: Determine LTS without doing a fetch, perhaps based on date
            // https://nodejs.org/en/about/releases/
            "^16".to_string()
        } else {
            versionish.clone()
        };

        NodeLayer { requirement }
    }
}

impl Layer for NodeLayer {
    type Buildpack = NodeBuildpack;
    type Metadata = LayerVersionMetadata;

    fn types(&self) -> LayerTypes {
        LayerTypes {
            build: true,
            launch: true,
            cache: true,
        }
    }

    fn existing_layer_strategy(
        &self,
        _context: &BuildContext<Self::Buildpack>,
        layer_data: &libcnb::layer::LayerData<Self::Metadata>,
    ) -> Result<libcnb::layer::ExistingLayerStrategy, <Self::Buildpack as Buildpack>::Error> {
        let installed = NodeBinary {}.semver_version_matches(
            &layer_data.content_metadata.metadata.version,
            &self.requirement,
        )?;

        let strategy = if installed {
            tracing::info!(
                "Existing `node` layer has `./bin/node` matching semver requirement `{}`; will keep",
                self.requirement
            );
            ExistingLayerStrategy::Keep
        } else {
            tracing::info!(
                "Existing `node` layer does not have `./bin/node` matching semver requirement `{}`; will recreate",
                self.requirement
            );
            ExistingLayerStrategy::Recreate
        };
        Ok(strategy)
    }

    fn create(
        &self,
        context: &BuildContext<Self::Buildpack>,
        layer_path: &Path,
    ) -> Result<LayerResult<Self::Metadata>, eyre::Report> {
        tracing::info!(
            "Creating `node` layer with semver requirement `{}`",
            self.requirement
        );

        let node = NodeBinary {}.require_sync(Some(self.requirement.clone()), true)?;
        let version = node.version()?.to_string();

        if is_local_build(context) {
            if node.is_stencila_install() {
                tracing::info!("Linking to `node {}` installed by Stencila", version);
                let source = node.grandparent()?;

                symlink_dir(source.join("bin"), &layer_path.join("bin"))?;
                symlink_dir(source.join("lib"), &layer_path.join("lib"))?;
            } else {
                tracing::info!("Linking to `node {}` installed on system", version);
                let source = node.parent()?;

                let bin_path = layer_path.join("bin");
                create_dir_all(&bin_path)?;
                symlink_file(node.path, bin_path.join(node.name))?;
                symlink_file(source.join("corepack"), bin_path.join("corepack"))?;
                symlink_file(source.join("npm"), bin_path.join("npm"))?;
                symlink_file(source.join("npx"), bin_path.join("npx"))?;

                let lib_path = layer_path.join("lib");
                create_dir_all(&lib_path)?;
                symlink_dir(
                    source.join("..").join("lib").join(NODE_MODULES),
                    lib_path.join(NODE_MODULES),
                )?;
            }
        } else {
            #[allow(clippy::collapsible_else_if)]
            if node.is_stencila_install() {
                tracing::info!("Moving `node {}` installed by Stencila", version);
                let source = node.grandparent()?;

                move_dir_all(&source, layer_path)?;
            } else {
                tracing::info!("Linking to `node {}` installed on stack image", version);
                let source = node.grandparent()?;

                symlink_dir(source.join("bin"), &layer_path.join("bin"))?;
                symlink_dir(source.join("lib"), &layer_path.join("lib"))?;
            }
        }

        LayerResultBuilder::new(LayerVersionMetadata { version }).build()
    }
}

struct NodeModulesLayer;

/// Generate `package_hash` string for an app directory
///
/// The hash is of the combined contens of `package-lock.json` and `package.json`.
/// This means that if either one is changed or removed that the hash will change.
fn generate_package_hash(app_dir: &Path) -> String {
    let content = [
        read_to_string(app_dir.join(PACKAGE_LOCK)).unwrap_or_default(),
        read_to_string(app_dir.join(PACKAGE_JSON)).unwrap_or_default(),
    ]
    .concat();
    str_sha256_hex(&content)
}

impl Layer for NodeModulesLayer {
    type Buildpack = NodeBuildpack;
    type Metadata = LayerHashMetadata;

    fn types(&self) -> LayerTypes {
        LayerTypes {
            build: true,
            launch: true,
            cache: true,
        }
    }

    fn existing_layer_strategy(
        &self,
        context: &BuildContext<Self::Buildpack>,
        layer_data: &libcnb::layer::LayerData<Self::Metadata>,
    ) -> Result<libcnb::layer::ExistingLayerStrategy, <Self::Buildpack as Buildpack>::Error> {
        let package_hash = generate_package_hash(&context.app_dir);
        let strategy = if package_hash == layer_data.content_metadata.metadata.hash {
            tracing::info!("Existing `node_modules` layer has same package hash; will keep",);
            ExistingLayerStrategy::Keep
        } else {
            tracing::info!("Existing `node_modules` layer has different package hash; will update");
            ExistingLayerStrategy::Update
        };
        Ok(strategy)
    }

    fn create(
        &self,
        context: &BuildContext<Self::Buildpack>,
        layer_path: &Path,
    ) -> Result<LayerResult<Self::Metadata>, eyre::Report> {
        tracing::info!("Creating `node_modules` layer");
        self.install(context, layer_path)
    }

    fn update(
        &self,
        context: &BuildContext<Self::Buildpack>,
        layer_data: &libcnb::layer::LayerData<Self::Metadata>,
    ) -> Result<LayerResult<Self::Metadata>, <Self::Buildpack as Buildpack>::Error> {
        tracing::info!("Updating `node_modules` layer");
        self.install(context, &layer_data.path)
    }
}

impl NodeModulesLayer {
    fn install(
        &self,
        context: &BuildContext<NodeBuildpack>,
        layer_path: &Path,
    ) -> Result<LayerResult<LayerHashMetadata>, eyre::Report> {
        let app_path = &context.app_dir;

        // Use the `node` and `npm` binaries installed in the `node` layer
        let node_layer = layer_path
            .parent()
            .expect("Should have parent")
            .join("node");

        // Prepend to the PATH because some package install scripts
        // use `/usr/bin/env node` which looks for `node` on PATH
        let mut envs: Vec<(OsString, OsString)> = vec![(
            "PATH".into(),
            NodeBuildpack::prepend_path(&node_layer.join("bin"))?,
        )];

        // If this is a CNB build use `layer_path/cache` as the NPM cache
        if is_cnb_build(context) {
            envs.push(("NPM_CONFIG_CACHE".into(), layer_path.join("cache").into()));
        }

        // Call the `npm-cli.js` script installed in the `node` layer
        // This is done, rather than executing `bin/npm` directly, there are issues with node `require`
        // module resolution when the latter is done.
        let node = BinaryInstallation::new(
            "node".into(),
            node_layer.join("bin").join("node"),
            None,
            envs,
        );
        let npm = node_layer
            .join("lib")
            .join(NODE_MODULES)
            .join("npm")
            .join("bin")
            .join("npm-cli.js")
            .into_os_string();

        let mut layer_env = LayerEnv::new();
        if is_local_build(context) {
            // Do the install in the app directory as normal
            node.run_sync(&[npm, "install".into()])?;
        } else {
            // Do the install in the layer.
            // Alternative, more complicated approaches to this e.g. doing a local install and then copying
            // over to layers and/or symlinking are problematic.

            // Despite some confusion online it seems that at present it is necessary to copy over these
            // files when using `--prefix`
            copy_if_exists(app_path.join(PACKAGE_JSON), layer_path.join(PACKAGE_JSON))?;
            copy_if_exists(app_path.join(PACKAGE_LOCK), layer_path.join(PACKAGE_LOCK))?;

            node.run_sync(&[npm, "install".into(), "--prefix".into(), layer_path.into()])?;

            // Set the `NODE_PATH` so that the `node_modules` can be found
            layer_env.insert(
                Scope::All,
                ModificationBehavior::Prepend,
                "NODE_PATH",
                layer_path.join(NODE_MODULES),
            );
        }

        // Generate a 'package hash' to detect if layer is stale in `existing_layer_strategy()`
        let metadata = LayerHashMetadata {
            hash: generate_package_hash(app_path),
        };

        LayerResultBuilder::new(metadata).env(layer_env).build()
    }
}
<commit_msg>refactor(Node buildpack): Minor refactorings including canonicalize for safety<commit_after>use std::{
    collections::HashMap,
    ffi::OsString,
    fs::{create_dir_all, read_to_string, remove_file},
    path::{Path, PathBuf},
};

use binary_node::{BinaryInstallation, BinaryTrait, NodeBinary};
use buildpack::{
    eyre,
    fs_utils::{copy_if_exists, move_dir_all, symlink_dir, symlink_file},
    hash_utils::str_sha256_hex,
    is_cnb_build, is_local_build,
    libcnb::{
        self,
        build::{BuildContext, BuildResult, BuildResultBuilder},
        data::{build_plan::BuildPlan, layer_content_metadata::LayerTypes, layer_name},
        detect::{DetectContext, DetectResult, DetectResultBuilder},
        generic::{GenericMetadata, GenericPlatform},
        layer::{ExistingLayerStrategy, Layer, LayerResult, LayerResultBuilder},
        layer_env::{LayerEnv, ModificationBehavior, Scope},
        Buildpack,
    },
    tracing, BuildpackTrait, LayerHashMetadata, LayerVersionMetadata, SYSTEM_INSTALLED,
};

pub struct NodeBuildpack;

impl BuildpackTrait for NodeBuildpack {
    fn toml() -> &'static str {
        include_str!("../buildpack.toml")
    }
}

const NODE_MODULES: &str = "node_modules";
const NVMRC: &str = ".nvmrc";
const PACKAGE_JSON: &str = "package.json";
const PACKAGE_LOCK: &str = "package-lock.json";
const TOOL_VERSIONS: &str = ".tool-versions";

impl Buildpack for NodeBuildpack {
    type Platform = GenericPlatform;
    type Metadata = GenericMetadata;
    type Error = eyre::Report;

    fn detect(&self, _context: DetectContext<Self>) -> libcnb::Result<DetectResult, Self::Error> {
        // Read `.tool-versions` for Node.js version
        let tool_versions = Self::tool_versions();

        // Read `.nvmrc` for Node.js version
        let nvmrc = read_to_string(NVMRC)
            .map(|content| content.trim().to_string())
            .ok();

        // Read `package.json` for Node.js version
        let package_json = read_to_string(PACKAGE_JSON)
            .ok()
            .and_then(|json| serde_json::from_str::<serde_json::Value>(&json).ok());

        // Detect `package-lock.json`
        let package_lock = PathBuf::from(PACKAGE_LOCK);

        // Fail early
        if !(tool_versions.contains_key("nodejs")
            || tool_versions.contains_key("node")
            || package_json.is_some()
            || package_lock.exists()
            || nvmrc.is_some()
            || Self::any_exist(&["main.js", "index.js"]))
        {
            return DetectResultBuilder::fail().build();
        }

        let mut requires = Vec::new();
        let mut provides = Vec::new();

        // Resolve Node.js version from `.tool-versions`, `.nvmrc`, `package.json`, or installed `node` version
        let (version, source) = if let Some(version) = tool_versions
            .get("nodejs")
            .or_else(|| tool_versions.get("node"))
        {
            (version.to_string(), TOOL_VERSIONS)
        } else if let Some(versionish) = nvmrc {
            (versionish, NVMRC)
        } else if let Some(semver) = package_json.as_ref().and_then(|package| {
            package
                .pointer("/engines/node")
                .and_then(|semver| semver.as_str().map(|semver| semver.to_string()))
        }) {
            (semver, PACKAGE_JSON)
        } else if let Some(version) = (NodeBinary {}).installed_version(None) {
            (version, SYSTEM_INSTALLED)
        } else {
            ("".to_string(), "")
        };

        // Require and provide Node.js
        let (require, provide) = Self::require_and_provide(
            format!("node {}", version).trim(),
            source,
            format!("Install Node.js {}", version).trim(),
        );
        requires.push(require);
        provides.push(provide);

        // Determine how NPM packages are to be installed
        if package_lock.exists() || package_json.is_some() {
            let (require, provide) = Self::require_and_provide(
                "node_modules",
                if package_lock.exists() {
                    PACKAGE_LOCK
                } else {
                    PACKAGE_JSON
                },
                "Install Node.js packages into `node_modules`",
            );
            requires.push(require);
            provides.push(provide);
        }

        let mut build_plan = BuildPlan::new();
        build_plan.requires = requires;
        build_plan.provides = provides;
        DetectResultBuilder::pass().build_plan(build_plan).build()
    }

    fn build(&self, context: BuildContext<Self>) -> libcnb::Result<BuildResult, Self::Error> {
        let entries: HashMap<_, _> = context
            .buildpack_plan
            .entries
            .iter()
            .map(|entry| Self::split_entry_name(&entry.name))
            .collect();

        if let Some(args) = entries.get("node") {
            context.handle_layer(layer_name!("node"), NodeLayer::new(args.clone()))?;
        }

        if entries.contains_key("node_modules") {
            context.handle_layer(layer_name!("node_modules"), NodeModulesLayer)?;
        }

        BuildResultBuilder::new().build()
    }
}

struct NodeLayer {
    /// The semantic version requirement for the `node` binary
    requirement: String,
}

impl NodeLayer {
    fn new(args: Vec<String>) -> Self {
        // Join args with commas because semver requirement parser expects that is
        // how parts of a requirement are separated
        let versionish = args.join(",");

        // Determine the semver requirement from versionish which could be a well formed semantic version (e.g 14.0.1),
        // a semver requirement (e.g. ^14.0), or an alias (e.g. `lts`)
        let requirement = if let Some(version) = versionish.strip_prefix('v') {
            version.to_string()
        } else if versionish == "lts" {
            // TODO: Determine LTS without doing a fetch, perhaps based on date
            // https://nodejs.org/en/about/releases/
            "^16".to_string()
        } else {
            versionish.clone()
        };

        NodeLayer { requirement }
    }
}

impl Layer for NodeLayer {
    type Buildpack = NodeBuildpack;
    type Metadata = LayerVersionMetadata;

    fn types(&self) -> LayerTypes {
        LayerTypes {
            build: true,
            launch: true,
            cache: true,
        }
    }

    fn existing_layer_strategy(
        &self,
        _context: &BuildContext<Self::Buildpack>,
        layer_data: &libcnb::layer::LayerData<Self::Metadata>,
    ) -> Result<libcnb::layer::ExistingLayerStrategy, <Self::Buildpack as Buildpack>::Error> {
        let installed = NodeBinary {}.semver_version_matches(
            &layer_data.content_metadata.metadata.version,
            &self.requirement,
        )?;
        let strategy = if installed {
            tracing::info!(
                "Existing `node` layer has `./bin/node` matching semver requirement `{}`; will keep",
                self.requirement
            );
            ExistingLayerStrategy::Keep
        } else {
            tracing::info!(
                "Existing `node` layer does not have `./bin/node` matching semver requirement `{}`; will recreate",
                self.requirement
            );
            ExistingLayerStrategy::Recreate
        };
        Ok(strategy)
    }

    fn create(
        &self,
        context: &BuildContext<Self::Buildpack>,
        layer_path: &Path,
    ) -> Result<LayerResult<Self::Metadata>, eyre::Report> {
        tracing::info!(
            "Creating `node` layer with semver requirement `{}`",
            self.requirement
        );

        let node = NodeBinary {}.require_sync(Some(self.requirement.clone()), true)?;
        let version = node.version()?.to_string();

        if is_local_build(context) {
            if node.is_stencila_install() {
                tracing::info!("Linking to `node {}` installed by Stencila", version);
                let source = node.grandparent()?;

                symlink_dir(source.join("bin"), &layer_path.join("bin"))?;
                symlink_dir(source.join("lib"), &layer_path.join("lib"))?;
            } else {
                tracing::info!("Linking to `node {}` installed on system", version);
                let source = node.parent()?;

                let bin_path = layer_path.join("bin");
                create_dir_all(&bin_path)?;
                symlink_file(node.path, bin_path.join(node.name))?;
                symlink_file(source.join("corepack"), bin_path.join("corepack"))?;
                symlink_file(source.join("npm"), bin_path.join("npm"))?;
                symlink_file(source.join("npx"), bin_path.join("npx"))?;

                let lib_path = layer_path.join("lib");
                create_dir_all(&lib_path)?;
                symlink_dir(
                    source.join("..").join("lib").join(NODE_MODULES),
                    lib_path.join(NODE_MODULES),
                )?;
            }
        } else {
            #[allow(clippy::collapsible_else_if)]
            if node.is_stencila_install() {
                tracing::info!("Moving `node {}` installed by Stencila", version);
                let source = node.grandparent()?;

                move_dir_all(&source, layer_path)?;
            } else {
                tracing::info!("Linking to `node {}` installed on stack image", version);
                let source = node.grandparent()?;

                symlink_dir(source.join("bin"), &layer_path.join("bin"))?;
                symlink_dir(source.join("lib"), &layer_path.join("lib"))?;
            }
        }

        LayerResultBuilder::new(LayerVersionMetadata { version }).build()
    }
}

struct NodeModulesLayer;

/// Generate `package_hash` string for an app directory
///
/// The hash is of the combined contens of `package-lock.json` and `package.json`.
/// This means that if either one is changed or removed that the hash will change.
fn generate_package_hash(app_dir: &Path) -> String {
    let content = [
        read_to_string(app_dir.join(PACKAGE_LOCK)).unwrap_or_default(),
        read_to_string(app_dir.join(PACKAGE_JSON)).unwrap_or_default(),
    ]
    .concat();
    str_sha256_hex(&content)
}

impl Layer for NodeModulesLayer {
    type Buildpack = NodeBuildpack;
    type Metadata = LayerHashMetadata;

    fn types(&self) -> LayerTypes {
        LayerTypes {
            build: true,
            launch: true,
            cache: true,
        }
    }

    fn existing_layer_strategy(
        &self,
        context: &BuildContext<Self::Buildpack>,
        layer_data: &libcnb::layer::LayerData<Self::Metadata>,
    ) -> Result<libcnb::layer::ExistingLayerStrategy, <Self::Buildpack as Buildpack>::Error> {
        let package_hash = generate_package_hash(&context.app_dir);
        let strategy = if package_hash == layer_data.content_metadata.metadata.hash {
            tracing::info!("Existing `node_modules` layer has same package hash; will keep",);
            ExistingLayerStrategy::Keep
        } else {
            tracing::info!("Existing `node_modules` layer has different package hash; will update");
            ExistingLayerStrategy::Update
        };
        Ok(strategy)
    }

    fn create(
        &self,
        context: &BuildContext<Self::Buildpack>,
        layer_path: &Path,
    ) -> Result<LayerResult<Self::Metadata>, eyre::Report> {
        tracing::info!("Creating `node_modules` layer");
        self.install(context, layer_path)
    }

    fn update(
        &self,
        context: &BuildContext<Self::Buildpack>,
        layer_data: &libcnb::layer::LayerData<Self::Metadata>,
    ) -> Result<LayerResult<Self::Metadata>, <Self::Buildpack as Buildpack>::Error> {
        tracing::info!("Updating `node_modules` layer");
        self.install(context, &layer_data.path)
    }
}

impl NodeModulesLayer {
    fn install(
        &self,
        context: &BuildContext<NodeBuildpack>,
        layer_path: &Path,
    ) -> Result<LayerResult<LayerHashMetadata>, eyre::Report> {
        let app_path = &context.app_dir.canonicalize()?;
        let layer_path = &layer_path.canonicalize()?;

        // Use the `node` and `npm` binaries installed in the `node` layer
        let node_layer = layer_path
            .parent()
            .expect("Should have parent")
            .join("node");

        // Prepend to the PATH because some package install scripts
        // use `/usr/bin/env node` which looks for `node` on PATH
        let mut envs: Vec<(OsString, OsString)> = vec![(
            "PATH".into(),
            NodeBuildpack::prepend_path(&node_layer.join("bin"))?,
        )];

        // If this is a CNB build use `layer_path/cache` as the NPM cache
        if is_cnb_build(context) {
            envs.push(("NPM_CONFIG_CACHE".into(), layer_path.join("cache").into()));
        }

        // Call the `npm-cli.js` script installed in the `node` layer
        // This is done, rather than executing `bin/npm` directly, there are issues with node `require`
        // module resolution when the latter is done.
        let node = BinaryInstallation::new("node", node_layer.join("bin").join("node"), None, envs);
        let npm = node_layer
            .join("lib")
            .join(NODE_MODULES)
            .join("npm")
            .join("bin")
            .join("npm-cli.js")
            .into_os_string();

        if is_local_build(context) {
            // Do the install in the app directory as normal
            node.run_sync([npm, "install".into()])?;
        } else {
            // Do the install in the layer.
            // Alternative, more complicated approaches to this e.g. doing a local install and then copying
            // over to layers and/or symlinking are problematic.

            // Despite some confusion online it seems that at present it is necessary to copy over these
            // files when using `--prefix`
            copy_if_exists(app_path.join(PACKAGE_JSON), layer_path.join(PACKAGE_JSON))?;
            copy_if_exists(app_path.join(PACKAGE_LOCK), layer_path.join(PACKAGE_LOCK))?;

            node.run_sync([npm, "install".into(), "--prefix".into(), layer_path.into()])?;

            // Remove the files, so they are not there next time
            remove_file(layer_path.join(PACKAGE_JSON)).ok();
            remove_file(layer_path.join(PACKAGE_LOCK)).ok();
        }

        // Set the `NODE_PATH` so that the `node_modules` can be found
        let layer_env = LayerEnv::new().chainable_insert(
            Scope::All,
            ModificationBehavior::Prepend,
            "NODE_PATH",
            layer_path.join(NODE_MODULES),
        );

        // Generate a 'package hash' to detect if layer is stale in `existing_layer_strategy()`
        let metadata = LayerHashMetadata {
            hash: generate_package_hash(app_path),
        };

        LayerResultBuilder::new(metadata).env(layer_env).build()
    }
}
<|endoftext|>


<commit_before>// pest. The Elegant Parser
// Copyright (c) 2018 Dragoș Tiselice
//
// Licensed under the Apache License, Version 2.0
// <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0> or the MIT
// license <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. All files in the project carrying such notice may not be copied,
// modified, or distributed except according to those terms.

use std::fmt;
use std::hash::{Hash, Hasher};
use std::ptr;
use std::rc::Rc;
use std::str;

use super::flat_pairs::{self, FlatPairs};
use super::pair::{self, Pair};
use super::queueable_token::QueueableToken;
use super::tokens::{self, Tokens};
use RuleType;

/// An iterator over [`Pair`]s. It is created by [`pest::state`] and [`Pair::into_inner`].
///
/// [`Pair`]: struct.Pair.html
/// [`pest::state`]: ../fn.state.html
/// [`Pair::into_inner`]: struct.Pair.html#method.into_inner
#[derive(Clone)]
pub struct Pairs<'i, R> {
    queue: Rc<Vec<QueueableToken<R>>>,
    input: &'i [u8],
    start: usize,
    end: usize
}

pub fn new<R: RuleType>(
    queue: Rc<Vec<QueueableToken<R>>>,
    input: &[u8],
    start: usize,
    end: usize
) -> Pairs<R> {
    Pairs {
        queue,
        input,
        start,
        end
    }
}

impl<'i, R: RuleType> Pairs<'i, R> {
    /// Captures a slice from the `&str` defined by the starting position of the first token `Pair`
    /// and the ending position of the last token `Pair` of the `Pairs`. This also captures
    /// the input between those two token `Pair`s.
    ///
    /// # Examples
    ///
    /// ```
    /// # use std::rc::Rc;
    /// # use pest;
    /// # #[allow(non_camel_case_types)]
    /// # #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
    /// enum Rule {
    ///     a,
    ///     b
    /// }
    ///
    /// let input = "a b";
    /// let pairs = pest::state(input, |state| {
    ///     // generating Token pairs with Rule::a and Rule::b ...
    /// #     state.rule(Rule::a, |s| s.match_string("a")).and_then(|s| s.skip(1))
    /// #         .and_then(|s| s.rule(Rule::b, |s| s.match_string("b")))
    /// }).unwrap();
    ///
    /// assert_eq!(pairs.as_str(), "a b");
    /// ```
    #[inline]
    pub fn as_str(&self) -> &'i str {
        let start = self.pos(self.start);
        let end = self.pos(self.end - 1);

        // Generated positions always come from Positions and are UTF-8 borders.
        unsafe { str::from_utf8_unchecked(&self.input[start..end]) }
    }

    /// Captures inner token `Pair`s and concatenates resulting `&str`s. This does not capture
    /// the input between token `Pair`s.
    ///
    /// # Examples
    ///
    /// ```
    /// # use std::rc::Rc;
    /// # use pest;
    /// # #[allow(non_camel_case_types)]
    /// # #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
    /// enum Rule {
    ///     a,
    ///     b
    /// }
    ///
    /// let input = "a b";
    /// let pairs = pest::state(input, |state| {
    ///     // generating Token pairs with Rule::a and Rule::b ...
    /// #     state.rule(Rule::a, |s| s.match_string("a")).and_then(|s| s.skip(1))
    /// #         .and_then(|s| s.rule(Rule::b, |s| s.match_string("b")))
    /// }).unwrap();
    ///
    /// assert_eq!(pairs.concat(), "ab");
    /// ```
    #[inline]
    pub fn concat(&self) -> String {
        self.clone().fold(String::new(), |string, pair| string + pair.as_str())
    }

    /// Flattens the `Pairs`.
    ///
    /// # Examples
    ///
    /// ```
    /// # use std::rc::Rc;
    /// # use pest;
    /// # #[allow(non_camel_case_types)]
    /// # #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
    /// enum Rule {
    ///     a,
    ///     b
    /// }
    ///
    /// let input = "";
    /// let pairs = pest::state(input, |state| {
    ///     // generating nested Token pair with Rule::b inside Rule::a
    /// #     state.rule(Rule::a, |state| {
    /// #         state.rule(Rule::b, |s| Ok(s))
    /// #     })
    /// }).unwrap();
    /// let tokens: Vec<_> = pairs.flatten().tokens().collect();
    ///
    /// assert_eq!(tokens.len(), 4);
    /// ```
    #[inline]
    pub fn flatten(self) -> FlatPairs<'i, R> {
        flat_pairs::new(self.queue, self.input, self.start, self.end)
    }

    /// Returns the `Tokens` for the `Pairs`.
    ///
    /// # Examples
    ///
    /// ```
    /// # use std::rc::Rc;
    /// # use pest;
    /// # #[allow(non_camel_case_types)]
    /// # #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
    /// enum Rule {
    ///     a
    /// }
    ///
    /// let input = "";
    /// let pairs = pest::state(input, |state| {
    ///     // generating Token pair with Rule::a ...
    /// #     state.rule(Rule::a, |s| Ok(s))
    /// }).unwrap();
    /// let tokens: Vec<_> = pairs.tokens().collect();
    ///
    /// assert_eq!(tokens.len(), 2);
    /// ```
    #[inline]
    pub fn tokens(self) -> Tokens<'i, R> {
        tokens::new(self.queue, self.input, self.start, self.end)
    }

    #[inline]
    pub fn peek(&self) -> Option<Pair<'i, R>> {
        if self.start < self.end {
            Some(pair::new(Rc::clone(&self.queue), self.input, self.start))
        } else {
            None
        }
    }

    fn pair(&self) -> usize {
        match self.queue[self.start] {
            QueueableToken::Start {
                end_token_index, ..
            } => end_token_index,
            _ => unreachable!()
        }
    }

    fn pair_from_end(&self) -> usize {
        match self.queue[self.end - 1] {
            QueueableToken::End {
                start_token_index, ..
            } => start_token_index,
            _ => unreachable!()
        }
    }

    fn pos(&self, index: usize) -> usize {
        match self.queue[index] {
            QueueableToken::Start { input_pos, .. } | QueueableToken::End { input_pos, .. } => {
                input_pos
            }
        }
    }
}

impl<'i, R: RuleType> Iterator for Pairs<'i, R> {
    type Item = Pair<'i, R>;

    fn next(&mut self) -> Option<Self::Item> {
        let pair = self.peek()?;
        self.start = self.pair() + 1;
        Some(pair)
    }
}

impl<'i, R: RuleType> DoubleEndedIterator for Pairs<'i, R> {
    fn next_back(&mut self) -> Option<Self::Item> {
        if self.end <= self.start {
            return None;
        }

        self.end = self.pair_from_end();

        let pair = pair::new(Rc::clone(&self.queue), self.input, self.end);

        Some(pair)
    }
}

impl<'i, R: RuleType> fmt::Debug for Pairs<'i, R> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        f.debug_list().entries(self.clone()).finish()
    }
}

impl<'i, R: RuleType> fmt::Display for Pairs<'i, R> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(
            f,
            "[{}]",
            self.clone()
                .map(|pair| format!("{}", pair))
                .collect::<Vec<_>>()
                .join(", ")
        )
    }
}

impl<'i, R: PartialEq> PartialEq for Pairs<'i, R> {
    fn eq(&self, other: &Pairs<'i, R>) -> bool {
        Rc::ptr_eq(&self.queue, &other.queue) && ptr::eq(self.input, other.input)
            && self.start == other.start && self.end == other.end
    }
}

impl<'i, R: Eq> Eq for Pairs<'i, R> {}

impl<'i, R: Hash> Hash for Pairs<'i, R> {
    fn hash<H: Hasher>(&self, state: &mut H) {
        (&*self.queue as *const Vec<QueueableToken<R>>).hash(state);
        (self.input as *const [u8]).hash(state);
        self.start.hash(state);
        self.end.hash(state);
    }
}

#[cfg(test)]
mod tests {
    use super::super::super::Parser;
    use super::super::super::macros::tests::*;

    #[test]
    fn as_str() {
        let pairs = AbcParser::parse(Rule::a, "abcde").unwrap();

        assert_eq!(pairs.as_str(), "abcde");
    }

    #[test]
    fn concat() {
        let pairs = AbcParser::parse(Rule::a, "abcde").unwrap();

        assert_eq!(pairs.concat(), "abce");
    }

    #[test]
    fn pairs_debug() {
        let pairs = AbcParser::parse(Rule::a, "abcde").unwrap();

        assert_eq!(
            format!("{:?}", pairs),
            "[\
                Pair { rule: a, span: Span { str: \"abc\", start: 0, end: 3 }, inner: [\
                    Pair { rule: b, span: Span { str: \"b\", start: 1, end: 2 }, inner: [] }\
                ] }, \
                Pair { rule: c, span: Span { str: \"e\", start: 4, end: 5 }, inner: [] }\
            ]"
        .to_owned()
        );
    }

    #[test]
    fn pairs_display() {
        let pairs = AbcParser::parse(Rule::a, "abcde").unwrap();

        assert_eq!(
            format!("{}", pairs),
            "[a(0, 3, [b(1, 2)]), c(4, 5)]".to_owned()
        );
    }

    #[test]
    fn iter_for_pairs() {
        let pairs = AbcParser::parse(Rule::a, "abcde").unwrap();
        assert_eq!(
            pairs.map(|p| p.as_rule()).collect::<Vec<Rule>>(),
            vec![Rule::a, Rule::c]
        );
    }

    #[test]
    fn double_ended_iter_for_pairs() {
        let pairs = AbcParser::parse(Rule::a, "abcde").unwrap();
        assert_eq!(
            pairs.rev().map(|p| p.as_rule()).collect::<Vec<Rule>>(),
            vec![Rule::c, Rule::a]
        );
    }
}
<commit_msg>Minimal docs for Pairs::peek<commit_after>// pest. The Elegant Parser
// Copyright (c) 2018 Dragoș Tiselice
//
// Licensed under the Apache License, Version 2.0
// <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0> or the MIT
// license <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. All files in the project carrying such notice may not be copied,
// modified, or distributed except according to those terms.

use std::fmt;
use std::hash::{Hash, Hasher};
use std::ptr;
use std::rc::Rc;
use std::str;

use super::flat_pairs::{self, FlatPairs};
use super::pair::{self, Pair};
use super::queueable_token::QueueableToken;
use super::tokens::{self, Tokens};
use RuleType;

/// An iterator over [`Pair`]s. It is created by [`pest::state`] and [`Pair::into_inner`].
///
/// [`Pair`]: struct.Pair.html
/// [`pest::state`]: ../fn.state.html
/// [`Pair::into_inner`]: struct.Pair.html#method.into_inner
#[derive(Clone)]
pub struct Pairs<'i, R> {
    queue: Rc<Vec<QueueableToken<R>>>,
    input: &'i [u8],
    start: usize,
    end: usize
}

pub fn new<R: RuleType>(
    queue: Rc<Vec<QueueableToken<R>>>,
    input: &[u8],
    start: usize,
    end: usize
) -> Pairs<R> {
    Pairs {
        queue,
        input,
        start,
        end
    }
}

impl<'i, R: RuleType> Pairs<'i, R> {
    /// Captures a slice from the `&str` defined by the starting position of the first token `Pair`
    /// and the ending position of the last token `Pair` of the `Pairs`. This also captures
    /// the input between those two token `Pair`s.
    ///
    /// # Examples
    ///
    /// ```
    /// # use std::rc::Rc;
    /// # use pest;
    /// # #[allow(non_camel_case_types)]
    /// # #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
    /// enum Rule {
    ///     a,
    ///     b
    /// }
    ///
    /// let input = "a b";
    /// let pairs = pest::state(input, |state| {
    ///     // generating Token pairs with Rule::a and Rule::b ...
    /// #     state.rule(Rule::a, |s| s.match_string("a")).and_then(|s| s.skip(1))
    /// #         .and_then(|s| s.rule(Rule::b, |s| s.match_string("b")))
    /// }).unwrap();
    ///
    /// assert_eq!(pairs.as_str(), "a b");
    /// ```
    #[inline]
    pub fn as_str(&self) -> &'i str {
        let start = self.pos(self.start);
        let end = self.pos(self.end - 1);

        // Generated positions always come from Positions and are UTF-8 borders.
        unsafe { str::from_utf8_unchecked(&self.input[start..end]) }
    }

    /// Captures inner token `Pair`s and concatenates resulting `&str`s. This does not capture
    /// the input between token `Pair`s.
    ///
    /// # Examples
    ///
    /// ```
    /// # use std::rc::Rc;
    /// # use pest;
    /// # #[allow(non_camel_case_types)]
    /// # #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
    /// enum Rule {
    ///     a,
    ///     b
    /// }
    ///
    /// let input = "a b";
    /// let pairs = pest::state(input, |state| {
    ///     // generating Token pairs with Rule::a and Rule::b ...
    /// #     state.rule(Rule::a, |s| s.match_string("a")).and_then(|s| s.skip(1))
    /// #         .and_then(|s| s.rule(Rule::b, |s| s.match_string("b")))
    /// }).unwrap();
    ///
    /// assert_eq!(pairs.concat(), "ab");
    /// ```
    #[inline]
    pub fn concat(&self) -> String {
        self.clone().fold(String::new(), |string, pair| string + pair.as_str())
    }

    /// Flattens the `Pairs`.
    ///
    /// # Examples
    ///
    /// ```
    /// # use std::rc::Rc;
    /// # use pest;
    /// # #[allow(non_camel_case_types)]
    /// # #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
    /// enum Rule {
    ///     a,
    ///     b
    /// }
    ///
    /// let input = "";
    /// let pairs = pest::state(input, |state| {
    ///     // generating nested Token pair with Rule::b inside Rule::a
    /// #     state.rule(Rule::a, |state| {
    /// #         state.rule(Rule::b, |s| Ok(s))
    /// #     })
    /// }).unwrap();
    /// let tokens: Vec<_> = pairs.flatten().tokens().collect();
    ///
    /// assert_eq!(tokens.len(), 4);
    /// ```
    #[inline]
    pub fn flatten(self) -> FlatPairs<'i, R> {
        flat_pairs::new(self.queue, self.input, self.start, self.end)
    }

    /// Returns the `Tokens` for the `Pairs`.
    ///
    /// # Examples
    ///
    /// ```
    /// # use std::rc::Rc;
    /// # use pest;
    /// # #[allow(non_camel_case_types)]
    /// # #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
    /// enum Rule {
    ///     a
    /// }
    ///
    /// let input = "";
    /// let pairs = pest::state(input, |state| {
    ///     // generating Token pair with Rule::a ...
    /// #     state.rule(Rule::a, |s| Ok(s))
    /// }).unwrap();
    /// let tokens: Vec<_> = pairs.tokens().collect();
    ///
    /// assert_eq!(tokens.len(), 2);
    /// ```
    #[inline]
    pub fn tokens(self) -> Tokens<'i, R> {
        tokens::new(self.queue, self.input, self.start, self.end)
    }

    /// Peek at the first inner `Pair` without changing the position of this iterator.
    #[inline]
    pub fn peek(&self) -> Option<Pair<'i, R>> {
        if self.start < self.end {
            Some(pair::new(Rc::clone(&self.queue), self.input, self.start))
        } else {
            None
        }
    }

    fn pair(&self) -> usize {
        match self.queue[self.start] {
            QueueableToken::Start {
                end_token_index, ..
            } => end_token_index,
            _ => unreachable!()
        }
    }

    fn pair_from_end(&self) -> usize {
        match self.queue[self.end - 1] {
            QueueableToken::End {
                start_token_index, ..
            } => start_token_index,
            _ => unreachable!()
        }
    }

    fn pos(&self, index: usize) -> usize {
        match self.queue[index] {
            QueueableToken::Start { input_pos, .. } | QueueableToken::End { input_pos, .. } => {
                input_pos
            }
        }
    }
}

impl<'i, R: RuleType> Iterator for Pairs<'i, R> {
    type Item = Pair<'i, R>;

    fn next(&mut self) -> Option<Self::Item> {
        let pair = self.peek()?;
        self.start = self.pair() + 1;
        Some(pair)
    }
}

impl<'i, R: RuleType> DoubleEndedIterator for Pairs<'i, R> {
    fn next_back(&mut self) -> Option<Self::Item> {
        if self.end <= self.start {
            return None;
        }

        self.end = self.pair_from_end();

        let pair = pair::new(Rc::clone(&self.queue), self.input, self.end);

        Some(pair)
    }
}

impl<'i, R: RuleType> fmt::Debug for Pairs<'i, R> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        f.debug_list().entries(self.clone()).finish()
    }
}

impl<'i, R: RuleType> fmt::Display for Pairs<'i, R> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(
            f,
            "[{}]",
            self.clone()
                .map(|pair| format!("{}", pair))
                .collect::<Vec<_>>()
                .join(", ")
        )
    }
}

impl<'i, R: PartialEq> PartialEq for Pairs<'i, R> {
    fn eq(&self, other: &Pairs<'i, R>) -> bool {
        Rc::ptr_eq(&self.queue, &other.queue) && ptr::eq(self.input, other.input)
            && self.start == other.start && self.end == other.end
    }
}

impl<'i, R: Eq> Eq for Pairs<'i, R> {}

impl<'i, R: Hash> Hash for Pairs<'i, R> {
    fn hash<H: Hasher>(&self, state: &mut H) {
        (&*self.queue as *const Vec<QueueableToken<R>>).hash(state);
        (self.input as *const [u8]).hash(state);
        self.start.hash(state);
        self.end.hash(state);
    }
}

#[cfg(test)]
mod tests {
    use super::super::super::Parser;
    use super::super::super::macros::tests::*;

    #[test]
    fn as_str() {
        let pairs = AbcParser::parse(Rule::a, "abcde").unwrap();

        assert_eq!(pairs.as_str(), "abcde");
    }

    #[test]
    fn concat() {
        let pairs = AbcParser::parse(Rule::a, "abcde").unwrap();

        assert_eq!(pairs.concat(), "abce");
    }

    #[test]
    fn pairs_debug() {
        let pairs = AbcParser::parse(Rule::a, "abcde").unwrap();

        assert_eq!(
            format!("{:?}", pairs),
            "[\
                Pair { rule: a, span: Span { str: \"abc\", start: 0, end: 3 }, inner: [\
                    Pair { rule: b, span: Span { str: \"b\", start: 1, end: 2 }, inner: [] }\
                ] }, \
                Pair { rule: c, span: Span { str: \"e\", start: 4, end: 5 }, inner: [] }\
            ]"
        .to_owned()
        );
    }

    #[test]
    fn pairs_display() {
        let pairs = AbcParser::parse(Rule::a, "abcde").unwrap();

        assert_eq!(
            format!("{}", pairs),
            "[a(0, 3, [b(1, 2)]), c(4, 5)]".to_owned()
        );
    }

    #[test]
    fn iter_for_pairs() {
        let pairs = AbcParser::parse(Rule::a, "abcde").unwrap();
        assert_eq!(
            pairs.map(|p| p.as_rule()).collect::<Vec<Rule>>(),
            vec![Rule::a, Rule::c]
        );
    }

    #[test]
    fn double_ended_iter_for_pairs() {
        let pairs = AbcParser::parse(Rule::a, "abcde").unwrap();
        assert_eq!(
            pairs.rev().map(|p| p.as_rule()).collect::<Vec<Rule>>(),
            vec![Rule::c, Rule::a]
        );
    }
}
<|endoftext|>


<commit_before>#![allow(clippy::expect_fun_call)]
use std::fs::File;
use std::io::prelude::*;
use std::path::{Path, PathBuf};

use crate::support::{database, project};

#[test]
fn run_infer_schema_without_docs() {
    test_print_schema("print_schema_simple_without_docs", vec![]);
}

#[test]
fn run_infer_schema() {
    test_print_schema("print_schema_simple", vec!["--with-docs"]);
}

#[test]
fn run_infer_schema_include() {
    test_print_schema(
        "print_schema_only_tables",
        vec!["--with-docs", "-o", "users1"],
    );
}

#[test]
fn run_infer_schema_include_regex() {
    test_print_schema(
        "print_schema_only_table_regexes",
        vec!["--with-docs", "-o", "users1"],
    );
}

#[test]
fn run_infer_schema_exclude() {
    test_print_schema(
        "print_schema_except_tables",
        vec!["--with-docs", "-e", "users1"],
    );
}

#[test]
fn run_infer_schema_exclude_regex() {
    test_print_schema(
        "print_schema_except_table_regexes",
        vec!["--with-docs", "-e", "users1"],
    );
}

#[test]
fn run_infer_schema_table_order() {
    test_print_schema("print_schema_table_order", vec!["--with-docs"]);
}

#[test]
fn run_infer_schema_column_order() {
    test_print_schema(
        "print_schema_column_order",
        vec!["--column-sorting", "name"],
    );
}

#[test]
fn run_infer_schema_compound_primary_key() {
    test_print_schema("print_schema_compound_primary_key", vec!["--with-docs"]);
}

#[test]
#[cfg(feature = "postgres")]
fn print_schema_specifying_schema_name() {
    test_print_schema(
        "print_schema_specifying_schema_name",
        vec!["--with-docs", "--schema", "custom_schema"],
    )
}

#[test]
#[cfg(feature = "postgres")]
fn print_schema_specifying_schema_name_with_foreign_keys() {
    test_print_schema(
        "print_schema_specifying_schema_name_with_foreign_keys",
        vec!["--with-docs", "--schema", "custom_schema"],
    )
}

#[test]
#[cfg(feature = "postgres")]
fn print_schema_with_compound_foreign_keys() {
    test_print_schema(
        "print_schema_with_compound_foreign_keys",
        vec!["--with-docs"],
    );
}

#[test]
#[cfg(feature = "postgres")]
fn print_schema_with_foreign_keys() {
    test_print_schema("print_schema_with_foreign_keys", vec!["--with-docs"]);
}

#[test]
#[cfg(feature = "postgres")]
fn print_schema_with_foreign_keys_reserved_names() {
    test_print_schema(
        "print_schema_with_foreign_keys_reserved_names",
        vec!["--with-docs"],
    );
}

#[test]
fn print_schema_column_renaming() {
    test_print_schema("print_schema_column_renaming", vec!["--with-docs"]);
}

#[test]
#[cfg(feature = "postgres")]
fn print_schema_type_renaming() {
    test_print_schema("print_schema_type_renaming", vec!["--with-docs"]);
}

#[test]
#[cfg(feature = "mysql")]
fn print_schema_unsigned() {
    test_print_schema("print_schema_unsigned", vec!["--with-docs"]);
}

#[test]
#[cfg(feature = "mysql")]
fn print_schema_datetime_for_mysql() {
    test_print_schema("print_schema_datetime_for_mysql", vec!["--with-docs"]);
}

#[test]
#[cfg(not(windows))]
fn print_schema_patch_file() {
    let path_to_patch_file = backend_file_path("print_schema_patch_file", "schema.patch");
    let path = path_to_patch_file.display().to_string();
    test_print_schema("print_schema_patch_file", vec!["--patch-file", &path]);
}

#[test]
fn print_schema_custom_types() {
    test_print_schema(
        "print_schema_custom_types",
        vec!["--import-types", "foo::*", "--import-types", "bar::*"],
    );
}

#[test]
fn print_schema_with_unmappable_names() {
    test_print_schema("print_schema_with_unmappable_names", vec!["--with-docs"]);
}

#[test]
#[cfg(feature = "postgres")]
fn print_schema_with_unmappable_names_and_schema_name() {
    test_print_schema(
        "print_schema_with_unmappable_names_and_schema_name",
        vec!["--with-docs", "--schema", "custom_schema"],
    )
}

#[test]
fn print_schema_with_separate_unique_constraint_and_foreign_key() {
    test_print_schema("print_schema_regression_test_for_2623", vec![])
}

#[test]
fn schema_file_is_relative_to_project_root() {
    let p = project("schema_file_is_relative_to_project_root")
        .folder("foo")
        .build();
    let _db = database(&p.database_url());

    p.command("setup").run();
    p.command("migration").arg("run").cd("foo").run();

    assert!(p.has_file("src/schema.rs"));
}

#[test]
#[cfg(feature = "postgres")]
fn print_schema_disabling_custom_type_works() {
    test_print_schema(
        "print_schema_disabling_custom_type_works",
        vec!["--no-generate-missing-sql-type-definitions"],
    )
}

#[test]
#[cfg(feature = "postgres")]
fn print_schema_default_is_to_generate_custom_types() {
    test_print_schema(
        "print_schema_default_is_to_generate_custom_types",
        vec!["--with-docs"],
    )
}

#[test]
#[cfg(feature = "postgres")]
fn print_schema_specifying_schema_name_with_custom_type() {
    test_print_schema(
        "print_schema_specifying_schema_name_with_custom_type",
        vec!["--with-docs", "--schema", "custom_schema"],
    )
}

#[test]
#[cfg(feature = "sqlite")]
fn print_schema_generated_columns() {
    test_print_schema("print_schema_generated_columns", vec![])
}

#[test]
#[cfg(feature = "sqlite")]
fn print_schema_generated_columns_with_generated_always() {
    test_print_schema("print_schema_generated_columns_generated_always", vec![])
}

#[test]
#[cfg(feature = "postgres")]
fn print_schema_array_type() {
    test_print_schema("print_schema_array_type", vec![])
}

#[test]
#[cfg(feature = "sqlite")]
fn print_schema_sqlite_implicit_foreign_key_reference() {
    test_print_schema("print_schema_sqlite_implicit_foreign_key_reference", vec![]);
}

#[test]
#[cfg(feature = "postgres")]
fn print_schema_respect_type_name_case() {
    test_print_schema("print_schema_respect_type_name_case", vec!["--with-docs"])
}

#[test]
#[cfg(any(feature = "postgres", feature = "mysql"))]
fn print_schema_comments() {
    test_print_schema("print_schema_comments", vec!["--with-docs"])
}

#[cfg(feature = "sqlite")]
const BACKEND: &str = "sqlite";
#[cfg(feature = "postgres")]
const BACKEND: &str = "postgres";
#[cfg(feature = "mysql")]
const BACKEND: &str = "mysql";

fn backend_file_path(test_name: &str, file: &str) -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("tests")
        .join("print_schema")
        .join(test_name)
        .join(BACKEND)
        .join(file)
}

fn test_print_schema(test_name: &str, args: Vec<&str>) {
    let test_path = Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("tests")
        .join("print_schema")
        .join(test_name);
    let p = project(test_name).build();
    let db = database(&p.database_url());

    p.command("setup").run();

    let schema = read_file(&backend_file_path(test_name, "schema.sql"));
    db.execute(&schema);

    let result = p.command("print-schema").args(args).run();

    assert!(result.is_success(), "Result was unsuccessful {:?}", result);
    let expected = read_file(&backend_file_path(test_name, "expected.rs")).replace("\r\n", "\n");

    let result = result.stdout().replace("\r\n", "\n");

    assert_diff!(&expected, &result, "\n", 0);

    test_print_schema_config(test_name, &test_path, schema, expected);
}

fn test_print_schema_config(test_name: &str, test_path: &Path, schema: String, expected: String) {
    let config = read_file(&test_path.join("diesel.toml"));
    let mut p = project(&format!("{}_config", test_name)).file("diesel.toml", &config);

    let patch_file = backend_file_path(test_name, "schema.patch");
    if patch_file.exists() {
        let patch_contents = read_file(&patch_file);
        p = p.file("schema.patch", &patch_contents);
    }

    let p = p.build();

    p.command("setup").run();
    p.create_migration("12345_create_schema", &schema, None);

    let result = p.command("migration").arg("run").run();
    assert!(result.is_success(), "Result was unsuccessful {:?}", result);

    let schema = p.file_contents("src/schema.rs").replace("\r\n", "\n");
    assert_diff!(&expected, &schema, "\n", 0);

    let result = p.command("print-schema").run();
    assert!(result.is_success(), "Result was unsuccessful {:?}", result);

    let result = result.stdout().replace("\r\n", "\n");
    assert_diff!(&expected, &result, "\n", 0);
}

fn read_file(path: &Path) -> String {
    let mut file = File::open(path).expect(&format!("Could not open {}", path.display()));
    let mut string = String::new();
    file.read_to_string(&mut string).unwrap();
    string
}
<commit_msg>Update diesel_cli/tests/print_schema.rs<commit_after>#![allow(clippy::expect_fun_call)]
use std::fs::File;
use std::io::prelude::*;
use std::path::{Path, PathBuf};

use crate::support::{database, project};

#[test]
fn run_infer_schema_without_docs() {
    test_print_schema("print_schema_simple_without_docs", vec![]);
}

#[test]
fn run_infer_schema() {
    test_print_schema("print_schema_simple", vec!["--with-docs"]);
}

#[test]
fn run_infer_schema_include() {
    test_print_schema(
        "print_schema_only_tables",
        vec!["--with-docs", "-o", "users1"],
    );
}

#[test]
fn run_infer_schema_include_regex() {
    test_print_schema(
        "print_schema_only_table_regexes",
        vec!["--with-docs", "-o", "users1"],
    );
}

#[test]
fn run_infer_schema_exclude() {
    test_print_schema(
        "print_schema_except_tables",
        vec!["--with-docs", "-e", "users1"],
    );
}

#[test]
fn run_infer_schema_exclude_regex() {
    test_print_schema(
        "print_schema_except_table_regexes",
        vec!["--with-docs", "-e", "users1"],
    );
}

#[test]
fn run_infer_schema_table_order() {
    test_print_schema("print_schema_table_order", vec!["--with-docs"]);
}

#[test]
fn run_infer_schema_column_order() {
    test_print_schema(
        "print_schema_column_order",
        vec!["--column-sorting", "name"],
    );
}

#[test]
fn run_infer_schema_compound_primary_key() {
    test_print_schema("print_schema_compound_primary_key", vec!["--with-docs"]);
}

#[test]
#[cfg(feature = "postgres")]
fn print_schema_specifying_schema_name() {
    test_print_schema(
        "print_schema_specifying_schema_name",
        vec!["--with-docs", "--schema", "custom_schema"],
    )
}

#[test]
#[cfg(feature = "postgres")]
fn print_schema_specifying_schema_name_with_foreign_keys() {
    test_print_schema(
        "print_schema_specifying_schema_name_with_foreign_keys",
        vec!["--with-docs", "--schema", "custom_schema"],
    )
}

#[test]
#[cfg(feature = "postgres")]
fn print_schema_with_compound_foreign_keys() {
    test_print_schema(
        "print_schema_with_compound_foreign_keys",
        vec!["--with-docs"],
    );
}

#[test]
#[cfg(feature = "postgres")]
fn print_schema_with_foreign_keys() {
    test_print_schema("print_schema_with_foreign_keys", vec!["--with-docs"]);
}

#[test]
#[cfg(feature = "postgres")]
fn print_schema_with_foreign_keys_reserved_names() {
    test_print_schema(
        "print_schema_with_foreign_keys_reserved_names",
        vec!["--with-docs"],
    );
}

#[test]
fn print_schema_column_renaming() {
    test_print_schema("print_schema_column_renaming", vec!["--with-docs"]);
}

#[test]
#[cfg(feature = "postgres")]
fn print_schema_type_renaming() {
    test_print_schema("print_schema_type_renaming", vec!["--with-docs"]);
}

#[test]
#[cfg(feature = "mysql")]
fn print_schema_unsigned() {
    test_print_schema("print_schema_unsigned", vec!["--with-docs"]);
}

#[test]
#[cfg(feature = "mysql")]
fn print_schema_datetime_for_mysql() {
    test_print_schema("print_schema_datetime_for_mysql", vec!["--with-docs"]);
}

#[test]
#[cfg(not(windows))]
fn print_schema_patch_file() {
    let path_to_patch_file = backend_file_path("print_schema_patch_file", "schema.patch");
    let path = path_to_patch_file.display().to_string();
    test_print_schema("print_schema_patch_file", vec!["--patch-file", &path]);
}

#[test]
fn print_schema_custom_types() {
    test_print_schema(
        "print_schema_custom_types",
        vec!["--import-types", "foo::*", "--import-types", "bar::*"],
    );
}

#[test]
fn print_schema_with_unmappable_names() {
    test_print_schema("print_schema_with_unmappable_names", vec!["--with-docs"]);
}

#[test]
#[cfg(feature = "postgres")]
fn print_schema_with_unmappable_names_and_schema_name() {
    test_print_schema(
        "print_schema_with_unmappable_names_and_schema_name",
        vec!["--with-docs", "--schema", "custom_schema"],
    )
}

#[test]
fn print_schema_with_separate_unique_constraint_and_foreign_key() {
    test_print_schema("print_schema_regression_test_for_2623", vec![])
}

#[test]
fn schema_file_is_relative_to_project_root() {
    let p = project("schema_file_is_relative_to_project_root")
        .folder("foo")
        .build();
    let _db = database(&p.database_url());

    p.command("setup").run();
    p.command("migration").arg("run").cd("foo").run();

    assert!(p.has_file("src/schema.rs"));
}

#[test]
#[cfg(feature = "postgres")]
fn print_schema_disabling_custom_type_works() {
    test_print_schema(
        "print_schema_disabling_custom_type_works",
        vec!["--no-generate-missing-sql-type-definitions"],
    )
}

#[test]
#[cfg(feature = "postgres")]
fn print_schema_default_is_to_generate_custom_types() {
    test_print_schema(
        "print_schema_default_is_to_generate_custom_types",
        vec!["--with-docs"],
    )
}

#[test]
#[cfg(feature = "postgres")]
fn print_schema_specifying_schema_name_with_custom_type() {
    test_print_schema(
        "print_schema_specifying_schema_name_with_custom_type",
        vec!["--with-docs", "--schema", "custom_schema"],
    )
}

#[test]
#[cfg(feature = "sqlite")]
fn print_schema_generated_columns() {
    test_print_schema("print_schema_generated_columns", vec![])
}

#[test]
#[cfg(feature = "sqlite")]
fn print_schema_generated_columns_with_generated_always() {
    test_print_schema("print_schema_generated_columns_generated_always", vec![])
}

#[test]
#[cfg(feature = "postgres")]
fn print_schema_array_type() {
    test_print_schema("print_schema_array_type", vec![])
}

#[test]
#[cfg(feature = "sqlite")]
fn print_schema_sqlite_implicit_foreign_key_reference() {
    test_print_schema("print_schema_sqlite_implicit_foreign_key_reference", vec![]);
}

#[test]
#[cfg(feature = "postgres")]
fn print_schema_respects_type_name_case() {
    test_print_schema("print_schema_respects_type_name_case", vec!["--with-docs"])
}

#[test]
#[cfg(any(feature = "postgres", feature = "mysql"))]
fn print_schema_comments() {
    test_print_schema("print_schema_comments", vec!["--with-docs"])
}

#[cfg(feature = "sqlite")]
const BACKEND: &str = "sqlite";
#[cfg(feature = "postgres")]
const BACKEND: &str = "postgres";
#[cfg(feature = "mysql")]
const BACKEND: &str = "mysql";

fn backend_file_path(test_name: &str, file: &str) -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("tests")
        .join("print_schema")
        .join(test_name)
        .join(BACKEND)
        .join(file)
}

fn test_print_schema(test_name: &str, args: Vec<&str>) {
    let test_path = Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("tests")
        .join("print_schema")
        .join(test_name);
    let p = project(test_name).build();
    let db = database(&p.database_url());

    p.command("setup").run();

    let schema = read_file(&backend_file_path(test_name, "schema.sql"));
    db.execute(&schema);

    let result = p.command("print-schema").args(args).run();

    assert!(result.is_success(), "Result was unsuccessful {:?}", result);
    let expected = read_file(&backend_file_path(test_name, "expected.rs")).replace("\r\n", "\n");

    let result = result.stdout().replace("\r\n", "\n");

    assert_diff!(&expected, &result, "\n", 0);

    test_print_schema_config(test_name, &test_path, schema, expected);
}

fn test_print_schema_config(test_name: &str, test_path: &Path, schema: String, expected: String) {
    let config = read_file(&test_path.join("diesel.toml"));
    let mut p = project(&format!("{}_config", test_name)).file("diesel.toml", &config);

    let patch_file = backend_file_path(test_name, "schema.patch");
    if patch_file.exists() {
        let patch_contents = read_file(&patch_file);
        p = p.file("schema.patch", &patch_contents);
    }

    let p = p.build();

    p.command("setup").run();
    p.create_migration("12345_create_schema", &schema, None);

    let result = p.command("migration").arg("run").run();
    assert!(result.is_success(), "Result was unsuccessful {:?}", result);

    let schema = p.file_contents("src/schema.rs").replace("\r\n", "\n");
    assert_diff!(&expected, &schema, "\n", 0);

    let result = p.command("print-schema").run();
    assert!(result.is_success(), "Result was unsuccessful {:?}", result);

    let result = result.stdout().replace("\r\n", "\n");
    assert_diff!(&expected, &result, "\n", 0);
}

fn read_file(path: &Path) -> String {
    let mut file = File::open(path).expect(&format!("Could not open {}", path.display()));
    let mut string = String::new();
    file.read_to_string(&mut string).unwrap();
    string
}
<|endoftext|>


<commit_before>use crate::command::Config;
use crate::page::{parse_page, DiffOp, Page};
use crate::test::helper::empty_env;
use std::env;
use std::path::{Path, PathBuf};

fn config(repo: &str, branch: Option<&str>, args: Vec<&str>) -> Config {
    let mut dir = env::current_dir().unwrap();
    dir.push(Path::new(".git"));
    let mut a = Vec::new();
    for arg in args {
        a.push(arg.to_string());
    }
    Config {
        repo: repo.to_string(),
        branch: branch.map(|s| s.to_string()),
        git_dir: dir,
        args: a,
        stdout: false,
        pull_request: false,
        env: empty_env(),
    }
}

#[test]
fn parse_empty_args() {
    let c = config("https://github.com/user/repo.git", None, vec![]);
    match parse_page(&c).unwrap() {
        Page::Open => { /* OK */ }
        _ => assert!(false),
    }
}

#[test]
fn parse_file_or_dir() {
    for &(ref entry, relative) in &[
        (Path::new(".").join("README.md"), "README.md"),
        (PathBuf::from("src"), "src"),
        (Path::new(".").join("src").join("main.rs"), "src/main.rs"),
    ] {
        let c = config(
            "https://github.com/user/repo.git",
            None,
            vec![&entry.to_str().unwrap()],
        );
        match parse_page(&c).unwrap() {
            Page::FileOrDir {
                relative_path,
                hash,
                line: None,
            } => {
                assert_eq!(relative_path, relative);
                assert!(!hash.is_empty());
            }
            p => assert!(false, "Unexpected result: {:?}", p),
        }
    }
}

#[test]
fn parse_file_line() {
    for &(ref file, expected) in &[
        (Path::new(".").join("README.md#21"), Some(21)),
        (Path::new(".").join("src").join("main.rs#10"), Some(10)),
        (PathBuf::from("LICENSE.txt"), None),
    ] {
        let c = config(
            "https://github.com/user/repo.git",
            None,
            vec![&file.to_str().unwrap()],
        );
        match parse_page(&c).unwrap() {
            Page::FileOrDir { line, .. } => assert_eq!(line, expected, "input: {:?}", file),
            p => assert!(false, "Unexpected result: {:?}, input: {:?}", p, file),
        }
    }
}

#[test]
fn not_existing_file() {
    let c = config(
        "https://github.com/user/repo.git",
        None,
        vec!["not/existing/file.txt"],
    );
    assert!(parse_page(&c).is_err());
}

#[test]
fn file_at_specific_commit() {
    let c = config("https://github.com/user/repo.git", None, vec![&"README.md"]);
    let p = parse_page(&c).unwrap();
    let c = config(
        "https://github.com/user/repo.git",
        None,
        vec![&"README.md", "HEAD^"],
    );
    let p2 = parse_page(&c).unwrap();
    assert!(p != p2, format!("{:?} v.s. {:?}", p, p2));
}

#[test]
fn parse_commit_ref() {
    for &cm in &["HEAD", "HEAD~1", "HEAD^"] {
        let c = config("https://github.com/user/repo.git", None, vec![cm]);
        match parse_page(&c).unwrap() {
            Page::Commit { hash } => assert!(!hash.is_empty()),
            p => assert!(false, "Unexpected result: {:?}", p),
        }
    }
}

#[test]
#[cfg_attr(feature = "on-ci", ignore)]
fn parse_short_commit_hash() {
    for &(cm, expected) in &[
        ("499edbb", "499edbbbad4d8054e4a47e12944e5fb4a2ef7ec5"),
        (
            "bc869a14617a131fefe8fa1a3dcdeba0745880d5",
            "bc869a14617a131fefe8fa1a3dcdeba0745880d5",
        ),
    ] {
        let c = config("https://github.com/user/repo.git", None, vec![cm]);
        match parse_page(&c).unwrap() {
            Page::Commit { hash } => assert_eq!(hash, expected),
            p => assert!(false, "Unexpected result: {:?}", p),
        }
    }
}

#[test]
fn parse_diff_ref_name() {
    for &(arg, expected_op) in &[
        ("HEAD^..HEAD", DiffOp::TwoDots),
        ("HEAD^...HEAD", DiffOp::ThreeDots),
    ] {
        let c = config("https://github.com/user/repo.git", None, vec![arg]);
        match parse_page(&c).unwrap() {
            Page::Diff { lhs, rhs, op } => {
                assert!(!lhs.is_empty());
                assert!(!rhs.is_empty());
                assert_eq!(op, expected_op, "arg is {}", arg);
            }
            p => assert!(false, "Unexpected result: {:?}", p),
        }
    }
}

#[test]
#[cfg_attr(feature = "on-ci", ignore)]
fn parse_diff_revisions() {
    for &(arg, expected_op) in &[
        ("499edbb..bc869a1", DiffOp::TwoDots),
        ("499edbb...bc869a1", DiffOp::ThreeDots),
    ] {
        let c = config("https://github.com/user/repo.git", None, vec![arg]);
        match parse_page(&c).unwrap() {
            Page::Diff { lhs, rhs, op } => {
                assert_eq!(lhs, "499edbbbad4d8054e4a47e12944e5fb4a2ef7ec5");
                assert_eq!(rhs, "bc869a14617a131fefe8fa1a3dcdeba0745880d5");
                assert_eq!(op, expected_op, "arg is {}", arg);
            }
            p => assert!(false, "Unexpected result: {:?}", p),
        }
    }
}

#[test]
fn wrong_num_of_args() {
    let c = config(
        "https://github.com/user/repo.git",
        None,
        vec!["foo", "bar", "piyo"],
    );
    assert!(parse_page(&c).is_err());
}

#[test]
fn unknown_diff() {
    let c = config(
        "https://github.com/user/repo.git",
        None,
        vec!["HEAD~114514..HEAD~114513"],
    );
    assert!(parse_page(&c).is_err());
}

#[test]
fn file_for_unknown_commit() {
    let c = config(
        "https://github.com/user/repo.git",
        None,
        vec!["README.md", "HEAD~114514"],
    );
    assert!(parse_page(&c).is_err());
}

#[test]
fn diff_lhs_or_rhs_empty() {
    for ref path in &[
        Path::new("..").join("foo.txt"),
        Path::new("foo").join(".."),
        PathBuf::from(".."),
    ] {
        let c = config(
            "https://github.com/user/repo.git",
            None,
            vec![path.to_str().unwrap()],
        );
        match parse_page(&c) {
            Ok(p @ Page::Diff { .. }) => assert!(false, "Unexpectedly parsed as diff: {:?}", p),
            _ => { /*ok*/ }
        }
    }
}
<commit_msg>fix path separator in expected value of test case for Windows<commit_after>use crate::command::Config;
use crate::page::{parse_page, DiffOp, Page};
use crate::test::helper::empty_env;
use std::env;
use std::path::{Path, PathBuf};

fn config(repo: &str, branch: Option<&str>, args: Vec<&str>) -> Config {
    let mut dir = env::current_dir().unwrap();
    dir.push(Path::new(".git"));
    let mut a = Vec::new();
    for arg in args {
        a.push(arg.to_string());
    }
    Config {
        repo: repo.to_string(),
        branch: branch.map(|s| s.to_string()),
        git_dir: dir,
        args: a,
        stdout: false,
        pull_request: false,
        env: empty_env(),
    }
}

#[test]
fn parse_empty_args() {
    let c = config("https://github.com/user/repo.git", None, vec![]);
    match parse_page(&c).unwrap() {
        Page::Open => { /* OK */ }
        _ => assert!(false),
    }
}

#[test]
fn parse_file_or_dir() {
    for &(entry, relative) in &[
        (
            Path::new(".").join("README.md").as_path(),
            Path::new("README.md"),
        ),
        (Path::new("src"), Path::new("src")),
        (
            Path::new(".").join("src").join("main.rs").as_path(),
            Path::new("src").join("main.rs").as_path(),
        ),
    ] {
        let c = config(
            "https://github.com/user/repo.git",
            None,
            vec![&entry.to_str().unwrap()],
        );
        match parse_page(&c).unwrap() {
            Page::FileOrDir {
                relative_path,
                hash,
                line: None,
            } => {
                assert_eq!(relative_path, relative.to_str().unwrap());
                assert!(!hash.is_empty());
            }
            p => assert!(false, "Unexpected result: {:?}", p),
        }
    }
}

#[test]
fn parse_file_line() {
    for &(ref file, expected) in &[
        (Path::new(".").join("README.md#21"), Some(21)),
        (Path::new(".").join("src").join("main.rs#10"), Some(10)),
        (PathBuf::from("LICENSE.txt"), None),
    ] {
        let c = config(
            "https://github.com/user/repo.git",
            None,
            vec![&file.to_str().unwrap()],
        );
        match parse_page(&c).unwrap() {
            Page::FileOrDir { line, .. } => assert_eq!(line, expected, "input: {:?}", file),
            p => assert!(false, "Unexpected result: {:?}, input: {:?}", p, file),
        }
    }
}

#[test]
fn not_existing_file() {
    let c = config(
        "https://github.com/user/repo.git",
        None,
        vec!["not/existing/file.txt"],
    );
    assert!(parse_page(&c).is_err());
}

#[test]
fn file_at_specific_commit() {
    let c = config("https://github.com/user/repo.git", None, vec![&"README.md"]);
    let p = parse_page(&c).unwrap();
    let c = config(
        "https://github.com/user/repo.git",
        None,
        vec![&"README.md", "HEAD^"],
    );
    let p2 = parse_page(&c).unwrap();
    assert!(p != p2, format!("{:?} v.s. {:?}", p, p2));
}

#[test]
fn parse_commit_ref() {
    for &cm in &["HEAD", "HEAD~1", "HEAD^"] {
        let c = config("https://github.com/user/repo.git", None, vec![cm]);
        match parse_page(&c).unwrap() {
            Page::Commit { hash } => assert!(!hash.is_empty()),
            p => assert!(false, "Unexpected result: {:?}", p),
        }
    }
}

#[test]
#[cfg_attr(feature = "on-ci", ignore)]
fn parse_short_commit_hash() {
    for &(cm, expected) in &[
        ("499edbb", "499edbbbad4d8054e4a47e12944e5fb4a2ef7ec5"),
        (
            "bc869a14617a131fefe8fa1a3dcdeba0745880d5",
            "bc869a14617a131fefe8fa1a3dcdeba0745880d5",
        ),
    ] {
        let c = config("https://github.com/user/repo.git", None, vec![cm]);
        match parse_page(&c).unwrap() {
            Page::Commit { hash } => assert_eq!(hash, expected),
            p => assert!(false, "Unexpected result: {:?}", p),
        }
    }
}

#[test]
fn parse_diff_ref_name() {
    for &(arg, expected_op) in &[
        ("HEAD^..HEAD", DiffOp::TwoDots),
        ("HEAD^...HEAD", DiffOp::ThreeDots),
    ] {
        let c = config("https://github.com/user/repo.git", None, vec![arg]);
        match parse_page(&c).unwrap() {
            Page::Diff { lhs, rhs, op } => {
                assert!(!lhs.is_empty());
                assert!(!rhs.is_empty());
                assert_eq!(op, expected_op, "arg is {}", arg);
            }
            p => assert!(false, "Unexpected result: {:?}", p),
        }
    }
}

#[test]
#[cfg_attr(feature = "on-ci", ignore)]
fn parse_diff_revisions() {
    for &(arg, expected_op) in &[
        ("499edbb..bc869a1", DiffOp::TwoDots),
        ("499edbb...bc869a1", DiffOp::ThreeDots),
    ] {
        let c = config("https://github.com/user/repo.git", None, vec![arg]);
        match parse_page(&c).unwrap() {
            Page::Diff { lhs, rhs, op } => {
                assert_eq!(lhs, "499edbbbad4d8054e4a47e12944e5fb4a2ef7ec5");
                assert_eq!(rhs, "bc869a14617a131fefe8fa1a3dcdeba0745880d5");
                assert_eq!(op, expected_op, "arg is {}", arg);
            }
            p => assert!(false, "Unexpected result: {:?}", p),
        }
    }
}

#[test]
fn wrong_num_of_args() {
    let c = config(
        "https://github.com/user/repo.git",
        None,
        vec!["foo", "bar", "piyo"],
    );
    assert!(parse_page(&c).is_err());
}

#[test]
fn unknown_diff() {
    let c = config(
        "https://github.com/user/repo.git",
        None,
        vec!["HEAD~114514..HEAD~114513"],
    );
    assert!(parse_page(&c).is_err());
}

#[test]
fn file_for_unknown_commit() {
    let c = config(
        "https://github.com/user/repo.git",
        None,
        vec!["README.md", "HEAD~114514"],
    );
    assert!(parse_page(&c).is_err());
}

#[test]
fn diff_lhs_or_rhs_empty() {
    for ref path in &[
        Path::new("..").join("foo.txt"),
        Path::new("foo").join(".."),
        PathBuf::from(".."),
    ] {
        let c = config(
            "https://github.com/user/repo.git",
            None,
            vec![path.to_str().unwrap()],
        );
        match parse_page(&c) {
            Ok(p @ Page::Diff { .. }) => assert!(false, "Unexpectedly parsed as diff: {:?}", p),
            _ => { /*ok*/ }
        }
    }
}
<|endoftext|>


<commit_before>/// The tokenizer for the query interpreter

use error::*;
use std::rc::Rc;

#[derive(Debug, PartialEq, Eq)]
pub enum Token {
    Identifier {
        ident: String,
        next: Option<Rc<Token>>
    },

    Index {
        idx: i64,
        next: Option<Rc<Token>>
    }
}

pub fn tokenize_with_seperator(query: &String, seperator: char) -> Result<Token> {
    unimplemented!()
}

#[cfg(test)]
mod test {
    use error::*;
    use super::*;

    use std::ops::Deref;

    #[test]
    fn test_tokenize_empty_query_to_error() {
        let tokens = tokenize_with_seperator(&String::from(""), '.');
        assert!(tokens.is_err());
        let tokens = tokens.unwrap_err();

        let errkind = tokens.kind();
        assert!(is_match!(errkind, &ErrorKind::EmptyQueryError { .. }));
    }

    #[test]
    fn test_tokenize_single_token_query() {
        let tokens = tokenize_with_seperator(&String::from("example"), '.');
        assert!(tokens.is_ok());
        let tokens = tokens.unwrap();

        assert!(match tokens {
            Token::Identifier {
                ident: ident,
                next: None
            } => {
                assert_eq!(String::from("example"), ident);
                true
            },
            _ => false,
        });
    }

    #[test]
    fn test_tokenize_double_token_query() {
        let tokens = tokenize_with_seperator(&String::from("a.b"), '.');
        assert!(tokens.is_ok());
        let tokens = tokens.unwrap();

        assert!(match tokens {
            Token::Identifier { next: Some(ref next), .. } => {
                assert_eq!(1, Rc::strong_count(&next));
                assert_eq!("b", next.deref().identifier());
                match next.deref() {
                    &Token::Identifier { next: None, .. } => true,
                    _ => false
                }
            },
            _ => false,
        });
        assert_eq!("a", tokens.identifier());
    }

    #[test]
    fn test_tokenize_ident_then_array_query() {
        let tokens = tokenize_with_seperator(&String::from("a.[0]"), '.');
        assert!(tokens.is_ok());
        let tokens = tokens.unwrap();

        assert_eq!("a", tokens.identifier());
        assert!(match tokens {
            Token::Identifier { next: Some(ref next), .. } => {
                assert_eq!(1, Rc::strong_count(&next));
                match next.deref() {
                    &Token::Index { idx: 0, next: None } => true,
                    _ => false
                }
            },
            _ => false,
        });
    }

    #[test]
    fn test_tokenize_many_idents_then_array_query() {
        use std::ops::Deref;

        let tokens = tokenize_with_seperator(&String::from("a.b.c.[1000]"), '.');
        assert!(tokens.is_ok());
        let tokens = tokens.unwrap();

        assert_eq!("a", tokens.identifier());

        let expected =
            Token::Identifier {
                ident: String::from("a"),
                next: Some(Rc::new(Token::Identifier {
                    ident: String::from("b"),
                    next: Some(Rc::new(Token::Identifier {
                        ident: String::from("c"),
                        next: Some(Rc::new(Token::Index {
                            idx: 1000,
                            next: None,
                        })),
                    })),
                })),
            };

        assert_eq!(expected, tokens);
    }

    quickcheck! {
        fn test_array_index(i: i64) -> bool {
            match tokenize_with_seperator(&format!("[{}]", i), '.') {
                Ok(Token::Index {
                    idx: i,
                    next: None,
                }) => true,
                _ => false,
            }
        }
    }

}
<commit_msg>Add Token impl for tests<commit_after>/// The tokenizer for the query interpreter

use error::*;
use std::rc::Rc;

#[derive(Debug, PartialEq, Eq)]
pub enum Token {
    Identifier {
        ident: String,
        next: Option<Rc<Token>>
    },

    Index {
        idx: i64,
        next: Option<Rc<Token>>
    }
}

impl Token {

    #[cfg(test)]
    pub fn identifier(&self) -> &String {
        match self {
            &Token::Identifier { ident: ref ident, .. } => &ident,
            _ => unreachable!(),
        }
    }

    #[cfg(test)]
    pub fn idx(&self) -> i64 {
        match self {
            &Token::Index { idx: i, .. } => i,
            _ => unreachable!(),
        }
    }
}

pub fn tokenize_with_seperator(query: &String, seperator: char) -> Result<Token> {
    unimplemented!()
}

#[cfg(test)]
mod test {
    use error::*;
    use super::*;

    use std::ops::Deref;

    #[test]
    fn test_tokenize_empty_query_to_error() {
        let tokens = tokenize_with_seperator(&String::from(""), '.');
        assert!(tokens.is_err());
        let tokens = tokens.unwrap_err();

        let errkind = tokens.kind();
        assert!(is_match!(errkind, &ErrorKind::EmptyQueryError { .. }));
    }

    #[test]
    fn test_tokenize_single_token_query() {
        let tokens = tokenize_with_seperator(&String::from("example"), '.');
        assert!(tokens.is_ok());
        let tokens = tokens.unwrap();

        assert!(match tokens {
            Token::Identifier {
                ident: ident,
                next: None
            } => {
                assert_eq!(String::from("example"), ident);
                true
            },
            _ => false,
        });
    }

    #[test]
    fn test_tokenize_double_token_query() {
        let tokens = tokenize_with_seperator(&String::from("a.b"), '.');
        assert!(tokens.is_ok());
        let tokens = tokens.unwrap();

        assert!(match tokens {
            Token::Identifier { next: Some(ref next), .. } => {
                assert_eq!(1, Rc::strong_count(&next));
                assert_eq!("b", next.deref().identifier());
                match next.deref() {
                    &Token::Identifier { next: None, .. } => true,
                    _ => false
                }
            },
            _ => false,
        });
        assert_eq!("a", tokens.identifier());
    }

    #[test]
    fn test_tokenize_ident_then_array_query() {
        let tokens = tokenize_with_seperator(&String::from("a.[0]"), '.');
        assert!(tokens.is_ok());
        let tokens = tokens.unwrap();

        assert_eq!("a", tokens.identifier());
        assert!(match tokens {
            Token::Identifier { next: Some(ref next), .. } => {
                assert_eq!(1, Rc::strong_count(&next));
                match next.deref() {
                    &Token::Index { idx: 0, next: None } => true,
                    _ => false
                }
            },
            _ => false,
        });
    }

    #[test]
    fn test_tokenize_many_idents_then_array_query() {
        use std::ops::Deref;

        let tokens = tokenize_with_seperator(&String::from("a.b.c.[1000]"), '.');
        assert!(tokens.is_ok());
        let tokens = tokens.unwrap();

        assert_eq!("a", tokens.identifier());

        let expected =
            Token::Identifier {
                ident: String::from("a"),
                next: Some(Rc::new(Token::Identifier {
                    ident: String::from("b"),
                    next: Some(Rc::new(Token::Identifier {
                        ident: String::from("c"),
                        next: Some(Rc::new(Token::Index {
                            idx: 1000,
                            next: None,
                        })),
                    })),
                })),
            };

        assert_eq!(expected, tokens);
    }

    quickcheck! {
        fn test_array_index(i: i64) -> bool {
            match tokenize_with_seperator(&format!("[{}]", i), '.') {
                Ok(Token::Index {
                    idx: i,
                    next: None,
                }) => true,
                _ => false,
            }
        }
    }

}
<|endoftext|>