Repository: dotenv-linter/dotenv-linter
Pull request #213 (2020-06-12 16:12:19)

Windows support path canonicalize
To solve the [problem with the UNC prefix](https://github.com/dotenv-linter/dotenv-linter/issues/212#issuecomment-642741675) on Win, I added [dunce](https://crates.io/crates/dunce) deps (it is very lightweight).
This elegantly solves our problem at the moment :+1: 

<details>
<summary> Windows output </summary>

![new_out_win](https://user-images.githubusercontent.com/26835520/84523134-798a9b00-ace0-11ea-9d67-1646cd3bed27.jpg)

```
PS D:\workdir\dotenv-linter-win-x64> ./dotenv-linter ../test
..\test\incorrect.env:1 LeadingCharacter: Invalid leading character detected
..\test\incorrect.env:2 KeyWithoutValue: The RAILS_ENV key should be with a value or have an equal sign
..\test\incorrect.env:3 IncorrectDelimiter: The DB-NAME key has incorrect delimiter
..\test\incorrect.env:3 UnorderedKey: The DB-NAME key should go before the LOGGER_LEVEL key
..\test\incorrect.env:4 LowercaseKey: The DEbUG_hTTP key should be in uppercase
..\test\incorrect.env:4 UnorderedKey: The DEbUG_hTTP key should go before the LOGGER_LEVEL key
..\test\incorrect.env:6 ExtraBlankLine: Extra blank line detected
..\test\incorrect.env:7 UnorderedKey: The DB_NAME key should go before the DEbUG_hTTP key
PS D:\workdir\dotenv-linter-win-x64> ./dotenv-linter ..\test\correct.env
PS D:\workdir\dotenv-linter-win-x64> ./dotenv-linter ..\test\incorrect.env
..\test\incorrect.env:1 LeadingCharacter: Invalid leading character detected
..\test\incorrect.env:2 KeyWithoutValue: The RAILS_ENV key should be with a value or have an equal sign
..\test\incorrect.env:3 IncorrectDelimiter: The DB-NAME key has incorrect delimiter
..\test\incorrect.env:3 UnorderedKey: The DB-NAME key should go before the LOGGER_LEVEL key
..\test\incorrect.env:4 LowercaseKey: The DEbUG_hTTP key should be in uppercase
..\test\incorrect.env:4 UnorderedKey: The DEbUG_hTTP key should go before the LOGGER_LEVEL key
..\test\incorrect.env:6 ExtraBlankLine: Extra blank line detected
..\test\incorrect.env:7 UnorderedKey: The DB_NAME key should go before the DEbUG_hTTP key
```

</details>

I also had to fix several integration tests so that the _file separator_ was not tied to the platform.
Now, all tests passed and output looks good. 

<details>
<summary> Windows tests results </summary>

As file: 
[test-win.txt](https://github.com/dotenv-linter/dotenv-linter/files/4771905/test-win.txt)

<details>
<summary>As text</summary>

```
D:\projects\dotenv-linter>cargo test
   Compiling dotenv-linter v1.2.0 (D:\projects\dotenv-linter)
    Finished test [unoptimized + debuginfo] target(s) in 0.87s
     Running target\debug\deps\dotenv_linter-2e142e803f25891e.exe

running 84 tests
test checks::duplicated_key::tests::with_two_unique_keys_test ... ok
test checks::duplicated_key::tests::one_duplicated_and_one_unique_key_test ... ok
test checks::duplicated_key::tests::with_two_duplicated_keys_test ... ok
test checks::duplicated_key::tests::with_one_duplicated_key_test ... ok
test checks::ending_blank_line::tests::blank_line_rn ... ok
test checks::ending_blank_line::tests::no_blank_line ... ok
test checks::ending_blank_line::tests::blank_line ... ok
test checks::extra_blank_line::tests::no_blank_lines ... ok
test checks::extra_blank_line::tests::single_blank_line ... ok
test checks::extra_blank_line::tests::three_blank_lines ... ok
test checks::extra_blank_line::tests::two_blank_lines ... ok
test checks::incorrect_delimiter::tests::empty_run ... ok
test checks::incorrect_delimiter::tests::failing_run ... ok
test checks::incorrect_delimiter::tests::failing_with_whitespace_run ... ok
test checks::incorrect_delimiter::tests::leading_space_run ... ok
test checks::incorrect_delimiter::tests::short_run ... ok
test checks::incorrect_delimiter::tests::trailing_space_run ... ok
test checks::incorrect_delimiter::tests::unformatted_run ... ok
test checks::incorrect_delimiter::tests::working_run ... ok
test checks::incorrect_delimiter::tests::working_with_digits_run ... ok
test checks::key_without_value::tests::failing_run ... ok
test checks::key_without_value::tests::working_run_with_blank_line ... ok
test checks::key_without_value::tests::working_run_with_value ... ok
test checks::key_without_value::tests::working_run_without_value ... ok
test checks::leading_character::tests::blank_line ... ok
test checks::leading_character::tests::leading_asterisk ... ok
test checks::leading_character::tests::leading_dot ... ok
test checks::leading_character::tests::leading_number ... ok
test checks::leading_character::tests::leading_space ... ok
test checks::leading_character::tests::leading_tab ... ok
test checks::leading_character::tests::leading_underscore ... ok
test checks::leading_character::tests::normal ... ok
test checks::leading_character::tests::two_leading_spaces ... ok
test checks::lowercase_key::tests::failing_run_with_lowercase_key ... ok
test checks::lowercase_key::tests::failing_run_with_lowercase_letter ... ok
test checks::lowercase_key::tests::working_run ... ok
test checks::quote_character::tests::with_double_quote_test ... ok
test checks::quote_character::tests::with_no_quotes_test ... ok
test checks::quote_character::tests::with_single_quote_test ... ok
test checks::space_character::tests::failing_run ... ok
test checks::space_character::tests::failing_when_whitespace_after_equal_sign_run ... ok
test checks::space_character::tests::failing_when_whitespace_before_equal_sign_run ... ok
test checks::space_character::tests::working_empty_run ... ok
test checks::space_character::tests::working_leading_run ... ok
test checks::space_character::tests::working_no_equal_sign_run ... ok
test checks::space_character::tests::working_run ... ok
test checks::space_character::tests::working_trailing_run ... ok
test checks::tests::check_name_list ... ok
test checks::tests::run_with_comment_line_test ... ok
test checks::tests::run_with_empty_line_test ... ok
test checks::tests::run_with_empty_vec_test ... ok
test checks::tests::run_with_invalid_line_test ... ok
test checks::tests::run_with_valid_line_test ... ok
test checks::tests::run_without_blank_line_test ... ok
test checks::tests::skip_all_checks ... ok
test checks::tests::skip_one_check ... ok
test checks::trailing_whitespace::tests::failing_trailing_run ... ok
test checks::trailing_whitespace::tests::working_run ... ok
test checks::unordered_key::tests::one_key_test ... ok
test checks::unordered_key::tests::one_unordered_key_test ... ok
test checks::unordered_key::tests::two_ordered_and_two_unordered_keys_test ... ok
test checks::unordered_key::tests::two_ordered_keys_test ... ok
test checks::unordered_key::tests::two_unordered_keys_before_and_after_test ... ok
test checks::unordered_key::tests::two_unordered_keys_before_test ... ok
test common::tests::file_entry::from::path_without_file_test ... ok
test common::tests::file_entry::from::path_with_file_test ... ok
test common::tests::file_entry::is_env_file_test ... ok
test common::tests::line_entry::get_key::correct_line_test ... ok
test common::tests::line_entry::get_key::empty_line_test ... ok
test common::tests::line_entry::get_key::line_without_value_test ... ok
test common::tests::line_entry::get_key::missing_value_and_equal_sign_test ... ok
test common::tests::line_entry::get_value::correct_line_test ... ok
test common::tests::line_entry::get_value::empty_line_test ... ok
test common::tests::line_entry::get_value::line_without_key_test ... ok
test common::tests::line_entry::get_value::line_without_value_test ... ok
test common::tests::line_entry::get_value::missing_value_and_equal_sign_test ... ok
test common::tests::line_entry::is_empty_or_comment::run_with_comment_line_test ... ok
test common::tests::line_entry::is_empty_or_comment::run_with_empty_line_test ... ok
test common::tests::line_entry::is_empty_or_comment::run_with_not_comment_or_empty_line_test ... ok
test common::tests::line_entry::trimmed_string::line_with_spaces_test ... ok
test common::tests::line_entry::trimmed_string::line_with_tab_test ... ok
test common::tests::line_entry::trimmed_string::line_without_blank_chars_test ... ok
test common::tests::warning_fmt_test ... ok
test fs_utils::tests::test_relative_path ... ok

test result: ok. 84 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

     Running target\debug\deps\dotenv_linter-1f89931db1a4e2a3.exe

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

     Running target\debug\deps\cli_basic-2124f9951beebd2f.exe

running 3 tests
test checks_current_dir ... ok
test exits_with_0_on_no_errors ... ok
test checks_current_dir_with_dot_arg ... ok

test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

     Running target\debug\deps\cli_exclude-e62b562e22ea331c.exe

running 3 tests
test exclude_one_file ... ok
test exclude_two_files ... ok
test exclude_one_file_check_one_file ... ok

test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

     Running target\debug\deps\cli_specific-64d956c050ca0f0d.exe

running 6 tests
test checks_one_specific_file_twice ... ok
test checks_one_specific_file ... ok
test checks_one_specific_path ... ok
test checks_one_specific_file_and_one_path ... ok
test checks_two_specific_files ... ok
test checks_two_specific_paths ... ok

test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

     Running target\debug\deps\ending_blank_line-a7ea2cecf43146a3.exe

running 2 tests
test correct_files ... ok
test incorrect_files ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

     Running target\debug\deps\extra_blank_line-b673247075673a6c.exe

running 4 tests
test two_blank_lines_at_the_end ... ok
test two_blank_lines_at_the_beginning ... ok
test two_blank_lines_in_the_middle ... ok
test correct_files ... ok

test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

   Doc-tests dotenv-linter

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
```
</details>

</details>


> **For UNIX it's also good works. All tests passed and seems correct.**

<!--
Thank you for your pull request. Please provide a description above and review
the requirements below.

Bug fixes and new features should include tests.
-->

<!-- _Please make sure to review and check all of these items:_ -->

#### ✔ Checklist:
<!-- Remove items that do not apply. For completed items, change [ ] to [x]. -->

- [X] This PR has been added to [CHANGELOG.md](https://github.com/dotenv-linter/dotenv-linter/blob/master/CHANGELOG.md) (at the top of the list);
- [X] Tests for the changes have been added (for bug fixes / features);
- [X] Docs have been added / updated (for bug fixes / features).

<!-- _NOTE: these things are not required to open a PR and can be done afterwards / while the PR is open._ -->


DDtKey: @mgrachev It seems that the tests on github do not work due to non-code issues :thinking: 

CI output:  `##[error]failed to download `remove_dir_all v0.5.2``
codecov-commenter: # [Codecov](https://codecov.io/gh/dotenv-linter/dotenv-linter/pull/213?src=pr&el=h1) Report
> Merging [#213](https://codecov.io/gh/dotenv-linter/dotenv-linter/pull/213?src=pr&el=desc) into [master](https://codecov.io/gh/dotenv-linter/dotenv-linter/commit/76e7b7d23caef8f58ed06162f95ab40a5a476450&el=desc) will **decrease** coverage by `0.05%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/dotenv-linter/dotenv-linter/pull/213/graphs/tree.svg?width=650&height=150&src=pr&token=wQybePfHI1)](https://codecov.io/gh/dotenv-linter/dotenv-linter/pull/213?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #213      +/-   ##
==========================================
- Coverage   96.56%   96.51%   -0.06%     
==========================================
  Files          15       16       +1     
  Lines        1457     1462       +5     
==========================================
+ Hits         1407     1411       +4     
- Misses         50       51       +1     
```


| [Impacted Files](https://codecov.io/gh/dotenv-linter/dotenv-linter/pull/213?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/fs\_utils.rs](https://codecov.io/gh/dotenv-linter/dotenv-linter/pull/213/diff?src=pr&el=tree#diff-c3JjL2ZzX3V0aWxzLnJz) | `100.00% <100.00%> (ø)` | |
| [src/lib.rs](https://codecov.io/gh/dotenv-linter/dotenv-linter/pull/213/diff?src=pr&el=tree#diff-c3JjL2xpYi5ycw==) | `85.50% <100.00%> (-4.82%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/dotenv-linter/dotenv-linter/pull/213?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/dotenv-linter/dotenv-linter/pull/213?src=pr&el=footer). Last update [76e7b7d...46840dd](https://codecov.io/gh/dotenv-linter/dotenv-linter/pull/213?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

DDtKey: > @DDtKey fire Great job! Thank you! pray
> 
> Everything is fine, but before merging this PR I'd like to ensure that all tests for Windows pass.
> So I'll merge this PR after we add additional steps to build and run tests on Windows.

I’m a little busy the last couple of days, soon I’ll try to add step to CI.

Thank you very much for your help and comments, I am glad to participate in this! :rocket: 
DDtKey: Hmm, it seems we have new issue.

So far, I can’t reproduce a similar error on my PC (neither stable, nor nightly build)
DDtKey: @mgrachev It was necessary to use canonicalization of paths, in the case of github actions, it looks very much like a symbolic link was used and `current_dir` was not working correctly.
There is a [issue around this ](https://github.com/rust-lang/rust/issues/37868)

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 29ea82e..ca1caa8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,7 @@ # Changelog
 
 ## [Unreleased]
 ### 🚀 Added
+- Add support canonicalize path for Windows [#213](https://github.com/dotenv-linter/dotenv-linter/pull/213) ([@DDtKey](https://github.com/DDtKey))
 - Add build and test steps running on Windows [#216](https://github.com/dotenv-linter/dotenv-linter/pull/216) ([@mgrachev](https://github.com/mgrachev))
 
 ### 🔧 Changed
diff --git a/Cargo.lock b/Cargo.lock
index c54bc51..20e6da3 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -69,9 +69,15 @@ version = "2.0.0"
 dependencies = [
  "assert_cmd 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "dunce 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "dunce"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
 [[package]]
 name = "escargot"
 version = "0.5.0"
@@ -332,6 +338,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 "checksum clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9"
 "checksum difference 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198"
 "checksum doc-comment 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
+"checksum dunce 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b2641c4a7c0c4101df53ea572bffdc561c146f6c2eb09e4df02bc4811e3feeb4"
 "checksum escargot 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "74cf96bec282dcdb07099f7e31d9fed323bca9435a09aba7b6d99b7617bca96d"
 "checksum getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb"
 "checksum itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e"
diff --git a/Cargo.toml b/Cargo.toml
index d216c73..aace36a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -13,6 +13,9 @@ categories = ["command-line-utilities"]
 [dependencies]
 clap = "2.33.0"
 
+[target.'cfg(windows)'.dependencies]
+dunce = "1.0.1"
+
 [dev-dependencies]
 assert_cmd = "0.12.0"
 tempfile = "3.1.0"
diff --git a/src/fs_utils.rs b/src/fs_utils.rs
new file mode 100644
index 0000000..ca6ca2f
--- /dev/null
+++ b/src/fs_utils.rs
@@ -0,0 +1,114 @@
+use std::path::PathBuf;
+
+/// For the Windows platform, we need to remove the UNC prefix.
+#[cfg(windows)]
+pub use dunce::canonicalize;
+
+/// For other platforms - continue use `std:fs`
+#[cfg(not(windows))]
+pub use std::fs::canonicalize;
+
+/// Returns the relative path for `target_path` relative to `base_path`
+pub fn get_relative_path(target_path: &PathBuf, base_path: &PathBuf) -> Option<PathBuf> {
+    let comp_target: Vec<_> = target_path.components().collect();
+    let comp_base: Vec<_> = base_path.components().collect();
+
+    let mut i = 0;
+    for (b, t) in comp_base.iter().zip(comp_target.iter()) {
+        if b != t {
+            break;
+        }
+        i += 1;
+    }
+
+    let mut relative_path = PathBuf::new();
+
+    for _ in 0..(comp_base.len() - i) {
+        relative_path.push("..");
+    }
+    relative_path.extend(comp_target.get(i..)?);
+
+    Some(relative_path)
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use std::env::temp_dir;
+    use std::fs::{remove_file, File};
+
+    fn run_relative_path_asserts(assertions: Vec<(&str, &str, &str)>) {
+        for (target, base, relative) in assertions {
+            assert_eq!(
+                get_relative_path(&PathBuf::from(target), &PathBuf::from(base),),
+                Some(PathBuf::from(relative))
+            );
+        }
+    }
+
+    #[test]
+    #[cfg(not(windows))]
+    fn test_relative_path() {
+        let assertions = vec![
+            ("/a/.env", "/a", ".env"),
+            ("/a/b/.env", "/a", "b/.env"),
+            ("/.env", "/a/b/c", "../../../.env"),
+            ("/a/b/c/d/.env", "/a/b/e/f", "../../c/d/.env"),
+        ];
+
+        run_relative_path_asserts(assertions)
+    }
+
+    #[test]
+    #[cfg(windows)]
+    fn test_relative_path() {
+        let assertions = vec![
+            ("C:\\a\\.env", "C:\\a", ".env"),
+            ("\\a\\b\\.env", "\\a", "b\\.env"),
+            ("\\.env", "\\a\\b\\c", "..\\..\\..\\.env"),
+            ("C:\\a\\b\\c\\.env", "C:\\a\\b\\e\\f", "..\\..\\c\\.env"),
+            ("\\\\?\\C:\\a\\.env", "C:\\a\\b", "\\\\?\\C:\\a\\.env"),
+        ];
+
+        run_relative_path_asserts(assertions)
+    }
+
+    #[test]
+    #[cfg(not(windows))]
+    fn test_canonicalize() {
+        let path = temp_dir().join(".env");
+        File::create(&path).expect("file created");
+
+        let fs_canonical_path = std::fs::canonicalize(&path).expect("canonical path by std::fs");
+        let canonical_path = super::canonicalize(&path).expect("canonical path by fs_utils");
+
+        // The result of `fs_utils` must match `std::fs` for non-Windows platform
+        assert_eq!(canonical_path, fs_canonical_path);
+
+        remove_file(path).expect("temp file deleted");
+    }
+
+    #[test]
+    #[cfg(windows)]
+    fn test_canonicalize() {
+        const UNC_PREFIX: &str = "\\\\?\\";
+
+        let file_name = String::from(".env");
+        let path = temp_dir().join(&file_name);
+        File::create(&path).expect("create testfile");
+
+        let dunce_canonical_path = dunce::canonicalize(&path).expect("canonical path by `dunce`");
+        let canonical_path = super::canonicalize(&path).expect("canonical path by `fs_utils`");
+
+        let contains_unc = canonical_path
+            .to_str()
+            .filter(|path| path.contains(UNC_PREFIX))
+            .is_some();
+
+        // The result of `fs_utils` must match `dunce` on Windows
+        assert_eq!(canonical_path, dunce_canonical_path);
+        assert!(!contains_unc);
+
+        remove_file(path).expect("temp file deleted");
+    }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 126b8c2..5f10084 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -4,10 +4,11 @@
 use std::error::Error;
 use std::ffi::OsStr;
 use std::path::PathBuf;
-use std::{env, fs, process};
+use std::{env, process};
 
 mod checks;
 mod common;
+mod fs_utils;
 
 fn get_args(current_dir: &OsStr) -> clap::ArgMatches {
     clap::App::new(env!("CARGO_PKG_NAME"))
@@ -76,12 +77,12 @@ pub fn run() -> Result<Vec<Warning>, Box<dyn Error>> {
     if let Some(inputs) = args.values_of("input") {
         dirs = inputs
             .clone()
-            .filter_map(|s| fs::canonicalize(s).ok())
+            .filter_map(|s| fs_utils::canonicalize(s).ok())
             .filter(|p| p.is_dir())
             .collect();
 
         file_paths = inputs
-            .filter_map(|s| fs::canonicalize(s).ok())
+            .filter_map(|s| fs_utils::canonicalize(s).ok())
             .filter(|p| p.is_file())
             .collect();
     }
@@ -100,8 +101,9 @@ pub fn run() -> Result<Vec<Warning>, Box<dyn Error>> {
 
     // Removes files from paths if they should be excluded
     if let Some(excluded) = args.values_of("exclude") {
-        let excluded_paths: Vec<PathBuf> =
-            excluded.filter_map(|f| fs::canonicalize(f).ok()).collect();
+        let excluded_paths: Vec<PathBuf> = excluded
+            .filter_map(|f| fs_utils::canonicalize(f).ok())
+            .collect();
 
         file_paths.retain(|path| !excluded_paths.contains(&path));
     }
@@ -112,7 +114,7 @@ pub fn run() -> Result<Vec<Warning>, Box<dyn Error>> {
     let mut warnings: Vec<Warning> = Vec::new();
 
     for path in file_paths {
-        let relative_path = match get_relative_path(&path, &current_dir) {
+        let relative_path = match fs_utils::get_relative_path(&path, &current_dir) {
             Some(p) => p,
             None => continue,
         };
@@ -144,47 +146,3 @@ fn get_line_entries(fe: FileEntry, lines: Vec<String>) -> Vec<LineEntry> {
 
     entries
 }
-
-fn get_relative_path(target_path: &PathBuf, base_path: &PathBuf) -> Option<PathBuf> {
-    let comp_target: Vec<_> = target_path.components().collect();
-    let comp_base: Vec<_> = base_path.components().collect();
-
-    let mut i = 0;
-    for (b, t) in comp_base.iter().zip(comp_target.iter()) {
-        if b != t {
-            break;
-        }
-        i += 1;
-    }
-
-    let mut relative_path = PathBuf::new();
-
-    for _ in 0..(comp_base.len() - i) {
-        relative_path.push("..");
-    }
-    relative_path.extend(comp_target.get(i..)?);
-
-    Some(relative_path)
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn test_relative_path() {
-        let assertions = vec![
-            ("/a/.env", "/a", ".env"),
-            ("/a/b/.env", "/a", "b/.env"),
-            ("/.env", "/a/b/c", "../../../.env"),
-            ("/a/b/c/d/.env", "/a/b/e/f", "../../c/d/.env"),
-        ];
-
-        for (target, base, relative) in assertions {
-            assert_eq!(
-                get_relative_path(&PathBuf::from(target), &PathBuf::from(base),),
-                Some(PathBuf::from(relative))
-            );
-        }
-    }
-}


Test patch:
diff --git a/tests/cli_common/mod.rs b/tests/cli_common/mod.rs
index 0c03f43..80928d9 100644
--- a/tests/cli_common/mod.rs
+++ b/tests/cli_common/mod.rs
@@ -5,6 +5,12 @@
 use std::path::PathBuf;
 use tempfile::{tempdir, tempdir_in, TempDir};
 
+#[cfg(windows)]
+use dunce::canonicalize;
+
+#[cfg(not(windows))]
+use std::fs::canonicalize;
+
 /// Use to test commands in temporary directories
 pub struct TestDir {
     current_dir: TempDir,
@@ -80,7 +86,8 @@ pub fn test_command_success_with_args<I, S>(self, args: I)
         S: AsRef<OsStr>,
     {
         let mut cmd = Self::init_cmd();
-        cmd.current_dir(&self.current_dir)
+        let canonical_current_dir = canonicalize(&self.current_dir).expect("canonical current dir");
+        cmd.current_dir(&canonical_current_dir)
             .args(args)
             .assert()
             .success();
@@ -98,7 +105,8 @@ pub fn test_command_fail_with_args<I, S>(self, args: I, expected_output: String)
         S: AsRef<OsStr>,
     {
         let mut cmd = Self::init_cmd();
-        cmd.current_dir(&self.current_dir)
+        let canonical_current_dir = canonicalize(&self.current_dir).expect("canonical current dir");
+        cmd.current_dir(&canonical_current_dir)
             .args(args)
             .assert()
             .failure()
diff --git a/tests/cli_specific.rs b/tests/cli_specific.rs
index 2afa3be..f06e32e 100644
--- a/tests/cli_specific.rs
+++ b/tests/cli_specific.rs
@@ -2,6 +2,7 @@
 mod cli_common;
 
 use cli_common::TestDir;
+use std::path::Path;
 
 #[test]
 fn checks_one_specific_path() {
@@ -13,9 +14,11 @@ fn checks_one_specific_path() {
 
     let args = &[subdir.as_str()];
     let expected_output = format!(
-        "{}/{}:1 LeadingCharacter: Invalid leading character detected\n",
-        testdir.relative_path(&subdir),
-        testfile_2.shortname_as_str(),
+        "{}:1 LeadingCharacter: Invalid leading character detected\n",
+        Path::new(&testdir.relative_path(&subdir))
+            .join(testfile_2.shortname_as_str())
+            .to_str()
+            .expect("multi-platform path to test .env file")
     );
 
     testdir.test_command_fail_with_args(args, expected_output);
@@ -34,11 +37,13 @@ fn checks_two_specific_paths() {
 
     let args = &[subdir_1.as_str(), subdir_2.as_str()];
     let expected_output = format!(
-        "{}/{}:1 LeadingCharacter: Invalid leading character detected\n{}/{}:1 LeadingCharacter: Invalid leading character detected\n",
-        testdir.relative_path(&subdir_1),
-        testfile_2.shortname_as_str(),
-        testdir.relative_path(&subdir_2),
-        testfile_3.shortname_as_str(),
+        "{}:1 LeadingCharacter: Invalid leading character detected\n{}:1 LeadingCharacter: Invalid leading character detected\n",
+        Path::new(&testdir.relative_path(&subdir_1))
+            .join(testfile_2.shortname_as_str())
+            .to_str().expect("multi-platform path to test .env file"),
+        Path::new(&testdir.relative_path(&subdir_2))
+            .join(testfile_3.shortname_as_str())
+            .to_str().expect("multi-platform path to test .env file"),
     );
 
     testdir.test_command_fail_with_args(args, expected_output);
@@ -70,9 +75,10 @@ fn checks_two_specific_files() {
 
     let args = &[testfile_2.as_str(), testfile_3.as_str()];
     let expected_output = format!(
-        "{}/{}:2 DuplicatedKey: The FOO key is duplicated\n{}:1 SpaceCharacter: The line has spaces around equal sign\n",
-        testdir.relative_path(&subdir),
-        testfile_3.shortname_as_str(),
+        "{}:2 DuplicatedKey: The FOO key is duplicated\n{}:1 SpaceCharacter: The line has spaces around equal sign\n",
+        Path::new(&testdir.relative_path(&subdir))
+            .join(testfile_3.shortname_as_str())
+            .to_str().expect("multi-platform path to test .env file"),
         testfile_2.shortname_as_str(),
     );
 
@@ -90,9 +96,10 @@ fn checks_one_specific_file_and_one_path() {
 
     let args = &[testfile_2.as_str(), subdir.as_str()];
     let expected_output = format!(
-        "{}/{}:2 DuplicatedKey: The FOO key is duplicated\n{}:2 UnorderedKey: The BAR key should go before the FOO key\n",
-        testdir.relative_path(&subdir),
-        testfile_3.shortname_as_str(),
+        "{}:2 DuplicatedKey: The FOO key is duplicated\n{}:2 UnorderedKey: The BAR key should go before the FOO key\n",
+        Path::new(&testdir.relative_path(&subdir))
+            .join(testfile_3.shortname_as_str())
+            .to_str().expect("multi-platform path to test .env file"),
         testfile_2.shortname_as_str(),
     );