summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorNick Young <nick@nickwb.net>2019-10-02 16:56:49 +1000
committerKevin Song <chipbuster@users.noreply.github.com>2019-10-02 01:56:48 -0500
commit6621e4c859a3546070932318b3b31066c811e786 (patch)
tree718ff034652a82fd547d1514e466153cef6dfcd8
parentf14392b5eac10cc7aacd7beb81a982955c73c6fc (diff)
feat: Add dotnet module (#416)
Adds a .NET module, which preferentially parses local/git files to get the .NET version.
-rw-r--r--.github/workflows/continuous-integration.yml11
-rw-r--r--docs/config/README.md42
-rw-r--r--starship/src/configs/dotnet.rs31
-rw-r--r--starship/src/configs/mod.rs11
-rw-r--r--starship/src/module.rs3
-rw-r--r--starship/src/modules/directory.rs2
-rw-r--r--starship/src/modules/dotnet.rs314
-rw-r--r--starship/src/modules/mod.rs35
-rw-r--r--tests/Dockerfile13
-rw-r--r--tests/testsuite/dotnet.rs146
-rw-r--r--tests/testsuite/main.rs1
11 files changed, 584 insertions, 25 deletions
diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml
index 378149053..f202333a3 100644
--- a/.github/workflows/continuous-integration.yml
+++ b/.github/workflows/continuous-integration.yml
@@ -70,7 +70,12 @@ jobs:
- uses: actions/setup-python@master
with:
python-version: "3.6.9"
-
+
+ # Install dotnet at a fixed version
+ - uses: actions/setup-dotnet@master
+ with:
+ dotnet-version: "2.2.402"
+
# Run the ignored tests that expect the above setup
- uses: actions/checkout@master
- name: Run all tests
@@ -88,6 +93,8 @@ jobs:
- name: Fix file permissions
run: chmod -R a+w .
- name: Build the Docker image
- run: docker build -f tests/Dockerfile --tag starshipcommand/starship-test --cache-from starshipcommand/starship-test .
+ run:
+ docker build -f tests/Dockerfile --tag starshipcommand/starship-test --cache-from
+ starshipcommand/starship-test .
- name: Run tests in Docker
run: docker run --rm -v $(pwd):/src/starship starshipcommand/starship-test
diff --git a/docs/config/README.md b/docs/config/README.md
index 5e2cf8de6..2e087ac66 100644
--- a/docs/config/README.md
+++ b/docs/config/README.md
@@ -91,12 +91,13 @@ prompt_order = [
"git_state",
"git_status",
"package",
+ "dotnet",
+ "golang",
+ "java",
"nodejs",
+ "python",
"ruby",
"rust",
- "python",
- "golang",
- "java",
"nix_shell",
"memory_usage",
"aws",
@@ -317,6 +318,41 @@ it would have been `nixpkgs/pkgs`.
truncation_length = 8
```
+## Dotnet
+
+The `dotnet` module shows the relevant version of the .NET Core SDK for the current directory. If
+the SDK has been pinned in the current directory, the pinned version is shown. Otherwise the module
+shows the latest installed version of the SDK.
+
+This module will only be shown in your prompt when one of the following files are present in the
+current directory: `global.json`, `project.json`, `*.sln`, `*.csproj`, `*.fsproj`, `*.xproj`. You'll
+also need the .NET Core command-line tools installed in order to use it correctly.
+
+Internally, this module uses its own mechanism for version detection. Typically it is twice as fast
+as running `dotnet --version`, but it may show an incorrect version if your .NET project has an
+unusual directory layout. If accuracy is more important than speed, you can disable the mechanism by
+setting `heuristic = false` in the module options.
+
+### Options
+
+| Variable | Default | Description |
+| ----------- | ------------- | -------------------------------------------------------- |
+| `symbol` | `"•NET "` | The symbol used before displaying the version of dotnet. |
+| `style` | `"bold blue"` | The style for the module. |
+| `heuristic` | `true` | Use faster version detection to keep starship snappy. |
+| `disabled` | `false` | Disables the `dotnet` module. |
+
+### Example
+
+```toml
+# ~/.config/starship.toml
+
+[dotnet]
+symbol = "🥅 "
+style = "green"
+heuristic = false
+```
+
## Environment Variable
The `env_var` module displays the current value of a selected environment variable.
diff --git a/starship/src/configs/dotnet.rs b/starship/src/configs/dotnet.rs
new file mode 100644
index 000000000..084798c6d
--- /dev/null
+++ b/starship/src/configs/dotnet.rs
@@ -0,0 +1,31 @@
+use crate::config::{ModuleConfig, RootModuleConfig, SegmentConfig};
+
+use ansi_term::{Color, Style};
+use starship_module_config_derive::ModuleConfig;
+
+#[derive(Clone, ModuleConfig)]
+pub struct DotnetConfig<'a> {
+ pub symbol: SegmentConfig<'a>,
+ pub version: SegmentConfig<'a>,
+ pub style: Style,
+ pub heuristic: bool,
+ pub disabled: bool,
+}
+
+impl<'a> RootModuleConfig<'a> for DotnetConfig<'a> {
+ fn new() -> Self {
+ DotnetConfig {
+ symbol: SegmentConfig {
+ value: "•NET ",
+ style: None,
+ },
+ version: SegmentConfig {
+ value: "",
+ style: None,
+ },
+ style: Color::Blue.bold(),
+ heuristic: true,
+ disabled: false,
+ }
+ }
+}
diff --git a/starship/src/configs/mod.rs b/starship/src/configs/mod.rs
index bac18a3f9..dcf8bb009 100644
--- a/starship/src/configs/mod.rs
+++ b/starship/src/configs/mod.rs
@@ -1,4 +1,5 @@
pub mod battery;
+pub mod dotnet;
pub mod rust;
use crate::config::{ModuleConfig, RootModuleConfig};
@@ -26,12 +27,16 @@ impl<'a> RootModuleConfig<'a> for StarshipRootConfig<'a> {
"git_state",
"git_status",
"package",
+ // ↓ Toolchain version modules ↓
+ // (Let's keep these sorted alphabetically)
+ "dotnet",
+ "golang",
+ "java",
"nodejs",
+ "python",
"ruby",
"rust",
- "python",
- "golang",
- "java",
+ // ↑ Toolchain version modules ↑
"nix_shell",
"memory_usage",
"aws",
diff --git a/starship/src/module.rs b/starship/src/module.rs
index 8ebb89fd3..d14814eb0 100644
--- a/starship/src/module.rs
+++ b/starship/src/module.rs
@@ -5,6 +5,8 @@ use ansi_term::{ANSIString, ANSIStrings};
use std::fmt;
// List of all modules
+// Keep these ordered alphabetically.
+// Default ordering is handled in configs/mod.rs
pub const ALL_MODULES: &[&str] = &[
"aws",
#[cfg(feature = "battery")]
@@ -12,6 +14,7 @@ pub const ALL_MODULES: &[&str] = &[
"character",
"cmd_duration",
"directory",
+ "dotnet",
"env_var",
"git_branch",
"git_state",
diff --git a/starship/src/modules/directory.rs b/starship/src/modules/directory.rs
index 4bab47c71..fd9e74fd1 100644
--- a/starship/src/modules/directory.rs
+++ b/starship/src/modules/directory.rs
@@ -122,7 +122,7 @@ fn contract_path(full_path: &Path, top_level_path: &Path, top_level_replacement:
/// On non-Windows OS, does nothing
#[cfg(target_os = "windows")]
fn replace_c_dir(path: String) -> String {
- return path.replace("C:/", "/c");
+ path.replace("C:/", "/c")
}
/// Replaces "C://" with "/c/" within a Windows path
diff --git a/starship/src/modules/dotnet.rs b/starship/src/modules/dotnet.rs
new file mode 100644
index 000000000..d63256e56
--- /dev/null
+++ b/starship/src/modules/dotnet.rs
@@ -0,0 +1,314 @@
+use std::ffi::OsStr;
+use std::iter::Iterator;
+use std::ops::Deref;
+use std::path::{Path, PathBuf};
+use std::process::{Command, Stdio};
+use std::str;
+
+use super::{Context, Module};
+use crate::config::RootModuleConfig;
+use crate::configs::dotnet::DotnetConfig;
+
+type JValue = serde_json::Value;
+
+const GLOBAL_JSON_FILE: &str = "global.json";
+const PROJECT_JSON_FILE: &str = "project.json";
+
+/// A module which shows the latest (or pinned) version of the dotnet SDK
+///
+/// Will display if any of the following files are present in
+/// the current directory:
+/// global.json, project.json, *.sln, *.csproj, *.fsproj, *.xproj
+pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
+ let dotnet_files = get_local_dotnet_files(context).ok()?;
+ if dotnet_files.is_empty() {
+ return None;
+ }
+
+ let mut module = context.new_module("dotnet");
+ let config = DotnetConfig::try_load(module.config);
+
+ // Internally, this module uses its own mechanism for version detection.
+ // Typically it is twice as fast as running `dotnet --version`.
+ let enable_heuristic = config.heuristic;
+ let version = if enable_heuristic {
+ let repo_root = context
+ .get_repo()
+ .ok()
+ .and_then(|r| r.root.as_ref().map(PathBuf::as_path));
+ estimate_dotnet_version(&dotnet_files, &context.current_dir, repo_root)?
+ } else {
+ get_version_from_cli()?
+ };
+
+ module.set_style(config.style);
+ module.create_segment("symbol", &config.symbol);
+ module.create_segment("version", &config.version.with_value(&version.0));
+
+ Some(module)
+}
+
+fn estimate_dotnet_version<'a>(
+ files: &[DotNetFile<'a>],
+ current_dir: &Path,
+ repo_root: Option<&Path>,
+) -> Option<Version> {
+ let get_file_of_type = |t: FileType| files.iter().find(|f| f.file_type == t);
+
+ // It's important to check for a global.json or a solution file first,
+ // but otherwise we can take any relevant file. We'll take whichever is first.
+ let relevant_file = get_file_of_type(FileType::GlobalJson)
+ .or_else(|| get_file_of_type(FileType::SolutionFile))
+ .or_else(|| files.iter().next())?;
+
+ match relevant_file.file_type {
+ FileType::GlobalJson => {
+ get_pinned_sdk_version_from_file(relevant_file.path).or_else(get_latest_sdk_from_cli)
+ }
+ FileType::SolutionFile => {
+ // With this heuristic, we'll assume that a "global.json" won't
+ // be found in any directory above the solution file.
+ get_latest_sdk_from_cli()
+ }
+ _ => {
+ // If we see a dotnet project, we'll check a small number of neighboring
+ // directories to see if we can find a global.json. Otherwise, assume the
+ // latest SDK is in use.
+ try_find_nearby_global_json(current_dir, repo_root).or_else(get_latest_sdk_from_cli)
+ }
+ }
+}
+
+/// Looks for a `global.json` which may exist in one of the parent directories of the current path.
+/// If there is one present, and it contains valid version pinning information, then return that version.
+///
+/// The following places are scanned:
+/// - The parent of the current directory
+/// (Unless there is a git repository, and the parent is above the root of that repository)
+/// - The root of the git repository
+/// (If there is one)
+fn try_find_nearby_global_json(current_dir: &Path, repo_root: Option<&Path>) -> Option<Version> {
+ let current_dir_is_repo_root = repo_root.map(|r| r == current_dir).unwrap_or(false);
+ let parent_dir = if current_dir_is_repo_root {
+ // Don't scan the parent directory if it's above the root of a git repository
+ None
+ } else {
+ current_dir.parent()
+ };
+
+ // Check the parent directory, or otherwise the repository root, for a global.json
+ let mut check_dirs = parent_dir
+ .iter()
+ .chain(repo_root.iter())
+ .copied() // Copies the reference, not the Path itself
+ .collect::<Vec<&Path>>();
+
+ // The parent directory and repository root may be the same directory,
+ // so avoid checking it twice.
+ check_dirs.dedup();
+
+ check_dirs
+ .iter()
+ // repo_root may be the same as the current directory. We don't need to scan it again.
+ .filter(|&&d| d != current_dir)
+ .filter_map(|d| check_directory_for_global_json(d))
+ // This will lazily evaluate the first directory with a global.json
+ .next()
+}
+
+fn check_directory_for_global_json(path: &Path) -> Option<Version> {
+ let global_json_path = path.join(GLOBAL_JSON_FILE);
+ log::debug!(
+ "Checking if global.json exists at: {}",
+ &global_json_path.display()
+ );
+ if global_json_path.exists() {
+ get_pinned_sdk_version_from_file(&global_json_path)
+ } else {
+ None
+ }
+}
+
+fn get_pinned_sdk_version_from_file(path: &Path) -> Option<Version> {
+ let json_text = crate::utils::read_file(path).ok()?;
+ log::debug!(
+ "Checking if .NET SDK version is pinned in: {}",
+ path.display()
+ );
+ get_pinned_sdk_version(&json_text)
+}
+
+fn get_pinned_sdk_version(json: &str) -> Option<Version> {
+ let parsed_json: JValue = serde_json::from_str(json).ok()?;
+
+ match parsed_json {
+ JValue::Object(root) => {
+ let sdk = root.get("sdk")?;
+ match sdk {
+ JValue::Object(sdk) => {
+ let version = sdk.get("version")?;
+ match version {
+ JValue::String(version_string) => {
+ let mut buffer = String::with_capacity(version_string.len() + 1);
+ buffer.push('v');
+ buffer.push_str(version_string);
+ Some(Version(buffer))
+ }
+ _ => None,
+ }
+ }
+ _ => None,
+ }
+ }
+ _ => None,
+ }
+}
+
+fn get_local_dotnet_files<'a>(context: &'a Context) -> Result<Vec<DotNetFile<'a>>, std::io::Error> {
+ Ok(context
+ .get_dir_files()?
+ .iter()
+ .filter_map(|p| {
+ get_dotnet_file_type(p).map(|t| DotNetFile {
+ path: p.as_ref(),
+ file_type: t,
+ })
+ })
+ .collect())
+}
+
+fn get_dotnet_file_type(path: &Path) -> Option<FileType> {
+ let file_name_lower = map_str_to_lower(path.file_name());
+
+ match file_name_lower.as_ref().map(|f| f.as_ref()) {
+ Some(GLOBAL_JSON_FILE) => return Some(FileType::GlobalJson),
+ Some(PROJECT_JSON_FILE) => return Some(FileType::ProjectJson),
+ _ => (),
+ };
+
+ let extension_lower = map_str_to_lower(path.extension());
+
+ match extension_lower.as_ref().map(|f| f.as_ref()) {
+ Some("sln") => return Some(FileType::SolutionFile),
+ Some("csproj") | Some("fsproj") | Some("xproj") => return Some(FileType::ProjectFile),
+ _ => (),
+ };
+
+ None
+}
+
+fn map_str_to_lower(value: Option<&OsStr>) -> Option<String> {
+ Some(value?.to_str()?.to_ascii_lowercase())
+}
+
+fn get_version_from_cli() -> Option<Version> {
+ let version_output = match Command::new("dotnet").arg("--version").output() {
+ Ok(output) => output,
+ Err(e) => {
+ log::warn!("Failed to execute `dotnet --version`. {}", e);
+ return None;
+ }
+ };
+ let version = str::from_utf8(version_output.stdout.as_slice())
+ .ok()?
+ .trim();
+
+ let mut buffer = String::with_capacity(version.len() + 1);
+ buffer.push('v');
+ buffer.push_str(version);
+
+ Some(Version(buffer))
+}
+
+fn get_latest_sdk_from_cli() -> Option<Version> {
+ let mut cmd = Command::new("dotnet");
+ cmd.arg("--list-sdks")
+ .stdout(Stdio::piped())
+ .stderr(Stdio::null())
+ .stdin(Stdio::null());
+
+ let exit_code = match cmd.status() {
+ Ok(status) => status,
+ Err(e) => {
+ log::warn!("Failed to execute `dotnet --list-sdks`. {}", e);
+ return None;
+ }
+ };
+
+ if exit_code.success() {
+ let sdks_output = cmd.output().ok()?;
+ fn parse_failed<T>() -> Option<T> {
+ log::warn!("Unable to parse the output from `dotnet --list-sdks`.");
+ None
+ };
+ let latest_sdk = str::from_utf8(sdks_output.stdout.as_slice())
+ .ok()?
+ .lines()
+ .map(str::trim)
+ .filter(|l| !l.is_empty())
+ .last()
+ .or_else(parse_failed)?;
+ let take_until = latest_sdk.find('[').or_else(parse_failed)? - 1;
+ if take_until > 1 {
+ let version = &latest_sdk[..take_until];
+ let mut buffer = String::with_capacity(version.len() + 1);
+ buffer.push('v');
+ buffer.push_str(version);
+ Some(Version(buffer))
+ } else {
+ parse_failed()
+ }
+ } else {
+ // Older versions of the dotnet cli do not support the --list-sdks command
+ // So, if the status code indicates failure, fall back to `dotnet --version`
+ log::warn!(
+ "Received a non-success exit code from `dotnet --list-sdks`. \
+ Falling back to `dotnet --version`.",
+ );
+ get_version_from_cli()
+ }
+}
+
+struct DotNetFile<'a> {
+ path: &'a Path,
+ file_type: FileType,
+}
+
+#[derive(PartialEq)]
+enum FileType {
+ ProjectJson,
+ ProjectFile,
+ GlobalJson,
+ SolutionFile,
+}
+
+struct Version(String);
+
+impl Deref for Version {
+ type Target = String;
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+#[test]
+fn should_parse_version_from_global_json() {
+ let json_text = r#"
+ {
+ "sdk": {
+ "version": "1.2.3"
+ }
+ }
+ "#;
+
+ let version = get_pinned_sdk_version(json_text).unwrap();
+ assert_eq!("v1.2.3", version.0);
+}
+
+#[test]
+fn should_ignore_empty_global_json() {
+ let json_text = "{}";
+
+ let version = get_pinned_sdk_version(json_text);
+ assert!(version.is_none());
+}
diff --git a/starship/src/modules/mod.rs b/starship/src/modules/mod.rs
index d5174ada2..5a0b8ddcb 100644
--- a/starship/src/modules/mod.rs
+++ b/starship/src/modules/mod.rs
@@ -3,6 +3,7 @@ mod aws;
mod character;
mod cmd_duration;
mod directory;
+mod dotnet;
mod env_var;
mod git_branch;
mod git_state;
@@ -31,32 +32,34 @@ use crate::module::Module;
pub fn handle<'a>(module: &str, context: &'a Context) -> Option<Module<'a>> {
match module {
+ // Keep these ordered alphabetically.
+ // Default ordering is handled in configs/mod.rs
"aws" => aws::module(context),
+ #[cfg(feature = "battery")]
+ "battery" => battery::module(context),
"directory" => directory::module(context),
- "env_var" => env_var::module(context),
"character" => character::module(context),
- "nodejs" => nodejs::module(context),
- "rust" => rust::module(context),
- "python" => python::module(context),
- "ruby" => ruby::module(context),
- "golang" => golang::module(context),
- "line_break" => line_break::module(context),
- "package" => package::module(context),
+ "cmd_duration" => cmd_duration::module(context),
+ "dotnet" => dotnet::module(context),
+ "env_var" => env_var::module(context),
"git_branch" => git_branch::module(context),
"git_state" => git_state::module(context),
"git_status" => git_status::module(context),
- "kubernetes" => kubernetes::module(context),
- "username" => username::module(context),
- #[cfg(feature = "battery")]
- "battery" => battery::module(context),
- "cmd_duration" => cmd_duration::module(context),
+ "golang" => golang::module(context),
+ "hostname" => hostname::module(context),
"java" => java::module(context),
"jobs" => jobs::module(context),
+ "kubernetes" => kubernetes::module(context),
+ "line_break" => line_break::module(context),
+ "memory_usage" => memory_usage::module(context),
"nix_shell" => nix_shell::module(context),
- "hostname" => hostname::module(context),
+ "nodejs" => nodejs::module(context),
+ "package" => package::module(context),
+ "python" => python::module(context),
+ "ruby" => ruby::module(context),
+ "rust" => rust::module(context),
"time" => time::module(context),
- "memory_usage" => memory_usage::module(context),
-
+ "username" => username::module(context),
_ => {
eprintln!("Error: Unknown module {}. Use starship module --list to list out all supported modules.", module);
None
diff --git a/tests/Dockerfile b/tests/Dockerfile
index 9e93f596b..bb825a089 100644
--- a/tests/Dockerfile
+++ b/tests/Dockerfile
@@ -48,6 +48,19 @@ RUN curl https://pyenv.run | bash \
# Check that Python was correctly installed
RUN python --version
+# Install Dotnet
+ENV DOTNET_HOME /home/nonroot/dotnet
+ENV DOTNET_SDK_VERSION 2.2.402
+
+RUN mkdir -p "$DOTNET_HOME" \
+ && dotnet_download="$DOTNET_HOME/../dotnet.tar.gz" \
+ && curl -SL --output "$dotnet_download" https://dotnetcli.blob.core.windows.net/dotnet/Sdk/$DOTNET_SDK_VERSION/dotnet-sdk-$DOTNET_SDK_VERSION-linux-x64.tar.gz \
+ && tar -zxf "$dotnet_download" -C "$DOTNET_HOME" \
+ && rm "$dotnet_download"
+
+ENV PATH $DOTNET_HOME:$PATH
+RUN dotnet help
+
# Create blank project
RUN USER=nonroot cargo new --bin /src/starship
WORKDIR /src/starship
diff --git a/tests/testsuite/dotnet.rs b/tests/testsuite/dotnet.rs
new file mode 100644
index 000000000..d395b3846
--- /dev/null
+++ b/tests/testsuite/dotnet.rs
@@ -0,0 +1,146 @@
+use super::common;
+use std::fs::{DirBuilder, OpenOptions};
+use std::io::{self, Error, ErrorKind, Write};
+use std::process::{Command, Stdio};
+use tempfile::TempDir;
+
+#[test]
+#[ignore]
+fn shows_nothing_in_directory_with_zero_relevant_files() -> io::Result<()> {
+ let workspace = create_workspace(false)?;
+ expect_output(&workspace, ".", None)
+}
+
+#[test]
+#[ignore]
+fn shows_latest_in_directory_with_solution() -> io::Result<()> {
+ let workspace = create_workspace(false)?;
+ touch_path(&workspace, "solution.sln", None)?;
+ expect_output(&workspace, ".", Some("•NET v2.2.402"))
+}
+
+#[test]
+#[ignore]
+fn shows_latest_in_directory_with_csproj() -> io::Result<()> {
+ let workspace = create_workspace(false)?;
+ touch_path(&workspace, "project.csproj", None)?;
+ expect_output(&workspace, ".", Some("•NET v2.2.402"))
+}
+
+#[test]
+#[ignore]
+fn shows_latest_in_directory_with_fsproj() -> io::Result<()> {
+ let workspace = create_workspace(false)?;
+ touch_path(&workspace, "project.fsproj", None)?;
+ expect_output(&workspace, ".", Some("•NET v2.2.402"))
+}
+
+#[test]
+#[ignore]
+fn shows_latest_in_directory_with_xproj() -> io::Result<()> {
+ let workspace = create_workspace(false)?;
+ touch_path(&workspace, "project.xproj", None)?;
+ expect_output(&workspace, ".", Some("•NET v2.2.402"))
+}
+
+#[test]
+#[ignore]
+fn shows_latest_in_directory_with_project_json() -> io::Result<()> {
+ let workspace = create_workspace(false)?;
+ touch_path(&workspace, "project.json", None)?;
+ expect_output(&workspace, ".", Some("•NET v2.2.402"))
+}
+
+#[test]
+#[ignore]
+fn shows_pinned_in_directory_with_global_json() -> io::Result<()> {
+ let workspace = create_workspace(false)?;
+ let global_json = make_pinned_sdk_json("1.2.3");
+ touch_path(&workspace, "global.json", Some(&global_json))?;
+ expect_output(&workspace, ".", Some("•NET v1.2.3"))
+}
+
+#[test]
+#[ignore]
+fn shows_pinned_in_project_below_root_with_global_json() -> io::Result<()> {
+ let workspace = create_workspace(false)?;
+ let global_json = make_pinned_sdk_json("1.2.3");
+ touch_path(&workspace, "global.json", Some(&global_json))?;
+ touch_path(&workspace, "project/project.csproj", None)?;
+ expect_output(&workspace, "project", Some("•NET v1.2.3"))
+}
+
+#[test]
+#[ignore]
+fn shows_pinned_in_deeply_nested_project_within_repository() -> io::Result<()> {
+ let workspace = create_workspace(true)?;
+ let global_json = make_pinned_sdk_json("1.2.3");
+ touch_path(&workspace, "global.json", Some(&global_json))?;
+ touch_path(&workspace, "deep/path/to/project/project.csproj", None)?;
+ expect_output(&workspace, "deep/path/to/project", Some("•NET v1.2.3"))
+}
+
+fn create_workspace(is_repo: bool) -> io::Result<TempDir> {
+ let repo_dir = common::new_tempdir()?;
+
+ if is_repo {
+ let mut command = Command::new("git");
+ command
+ .args(&["init", "--quiet"])
+ .stdout(Stdio::null())
+ .stderr(Stdio::null())
+ .stdin(Stdio::null())
+ .current_dir(repo_dir.path());
+
+ if !command.status()?.success() {
+ return Err(Error::from(ErrorKind::Other));
+ }
+ }
+
+ Ok(repo_dir)
+}
+
+fn touch_path(workspace: &TempDir, relative_path: &str, contents: Option<&str>) -> io::Result<()> {
+ let path = workspace.path().join(relative_path);
+
+ DirBuilder::new().recursive(true).create(
+ path.parent()
+ .expect("Expected relative_path to be a file in a directory"),
+ )?;
+
+ let mut file = OpenOptions::new()
+ .write(true)
+ .create(true)
+ .truncate(true)
+ .open(&path)?;
+ write!(file, "{}", contents.unwrap_or(""))
+}
+
+fn make_pinned_sdk_json(version: &str) -> String {
+ let json_text = r#"
+ {
+ "sdk": {
+ "version": "INSERT_VERSION"
+ }
+ }
+ "#;
+ json_text.replace("INSERT_VERSION", version)
+}
+
+fn expect_output(workspace: &TempDir, run_from: &str, contains: Option<&str>) -> io::Result<()> {
+ let run_path = workspace.path().join(run_from);
+ let output = common::render_module("dotnet")
+ .current_dir(run_path)
+ .output()?;
+ let text = String::from_utf8(output.stdout).unwrap();
+
+ // This can be helpful for debugging
+ eprintln!("The dotnet module showed: {}", text);
+
+ match contains {
+ Some(contains) => assert!(text.contains(contains)),
+ None => assert!(text.is_empty()),
+ }
+
+ Ok(())
+}
diff --git a/tests/testsuite/main.rs b/tests/testsuite/main.rs
index 471f5332a..f96eadcfd 100644
--- a/tests/testsuite/main.rs
+++ b/tests/testsuite/main.rs
@@ -4,6 +4,7 @@ mod cmd_duration;
mod common;
mod configuration;
mod directory;
+mod dotnet;
mod env_var;
mod git_branch;
mod git_state;