Repository: dtolnay/trybuild (Rust)

Issue:
Inappropriate substitution of $DIR and $WORKSPACE
I'm trying to run tests on https://github.com/PyO3/pyo3 and running into some issues with output mismatch. This is with `trybuild = "=1.0.49"`. Note that the URL `https://github.com/PyO3/pyo3/issues/1632` is replaced with `https://github.com$DIR$WORKSPACE/issues/1632`.

```
EXPECTED:
┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
error: Python functions cannot have generic type parameters
 --> tests/ui/invalid_pyfunctions.rs:4:21
  |
4 | fn generic_function<T>(value: T) {}
  |                     ^

error: Python functions cannot have `impl Trait` arguments
 --> tests/ui/invalid_pyfunctions.rs:7:36
  |
7 | fn impl_trait_function(impl_trait: impl AsRef<PyAny>) {}
  |                                    ^^^^

error: `async fn` is not yet supported for Python functions.

Additional crates such as `pyo3-asyncio` can be used to integrate async Rust and Python. For more information, see https://github.com/PyO3/pyo3/issues/1632
  --> tests/ui/invalid_pyfunctions.rs:10:1
   |
10 | async fn async_function() {}
   | ^^^^^
┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈

ACTUAL OUTPUT:
┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
error: Python functions cannot have generic type parameters
 --> tests/ui/invalid_pyfunctions.rs:4:21
  |
4 | fn generic_function<T>(value: T) {}
  |                     ^

error: Python functions cannot have `impl Trait` arguments
 --> tests/ui/invalid_pyfunctions.rs:7:36
  |
7 | fn impl_trait_function(impl_trait: impl AsRef<PyAny>) {}
  |                                    ^^^^

error: `async fn` is not yet supported for Python functions.

Additional crates such as `pyo3-asyncio` can be used to integrate async Rust and Python. For more information, see https://github.com$DIR$WORKSPACE/issues/1632
  --> tests/ui/invalid_pyfunctions.rs:10:1
   |
10 | async fn async_function() {}
   | ^^^^^
┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
```


Discussion:
Maybe a solution for this would be to recognize the `<protocol>://` in the URL and skip over it?
Ooops this is a result of me developing in a Docker container with the code mounted at `/pyo3` - confirmed by making a test for this repo with a similar URL and the code mounted at `/trybuild`. I can fix that test by mounting to a different location that does not conflict with the URL like that.
I published a fix in 1.0.50 so that it should work even at `/pyo3`.



Patch:
diff --git a/src/normalize.rs b/src/normalize.rs
index 09576b3..a2111d1 100644
--- a/src/normalize.rs
+++ b/src/normalize.rs
@@ -316,13 +316,29 @@ fn replace_case_insensitive(line: &str, pattern: &str, replacement: &str) -> Str
     let line_lower = line.to_ascii_lowercase().replace('\\', "/");
     let pattern_lower = pattern.to_ascii_lowercase().replace('\\', "/");
     let mut replaced = String::with_capacity(line.len());
-    for (i, keep) in line_lower.split(&pattern_lower).enumerate() {
-        if i > 0 {
+
+    let line_lower = line_lower.as_str();
+    let mut split = line_lower.split(&pattern_lower);
+    let mut pos = 0;
+    let mut insert_replacement = false;
+    while let Some(keep) = split.next() {
+        if insert_replacement {
             replaced.push_str(replacement);
+            pos += pattern.len();
+        }
+        let keep = &line[pos..pos + keep.len()];
+        replaced.push_str(keep);
+        pos += keep.len();
+        insert_replacement = true;
+        if replaced.ends_with(|ch: char| ch.is_ascii_alphanumeric()) {
+            if let Some(ch) = line[pos..].chars().next() {
+                replaced.push(ch);
+                pos += ch.len_utf8();
+                split = line_lower[pos..].split(&pattern_lower);
+                insert_replacement = false;
+            }
         }
-        let begin = replaced.len() - i * replacement.len() + i * pattern.len();
-        let end = begin + keep.len();
-        replaced.push_str(&line[begin..end]);
     }
+
     replaced
 }


Test patch:
diff --git a/src/tests.rs b/src/tests.rs
index 79fe90e..e8ab77d 100644
--- a/src/tests.rs
+++ b/src/tests.rs
@@ -2,13 +2,13 @@ use crate::directory::Directory;
 use crate::run::PathDependency;
 
 macro_rules! test_normalize {
-    ($name:ident $original:literal $expected:literal) => {
+    ($name:ident $(DIR=$dir:literal)? $(WORKSPACE=$workspace:literal)? $original:literal $expected:literal) => {
         #[test]
         fn $name() {
             let context = super::Context {
                 krate: "trybuild000",
-                source_dir: &Directory::new("/git/trybuild/test_suite"),
-                workspace: &Directory::new("/git/trybuild"),
+                source_dir: &Directory::new({ "/git/trybuild/test_suite" $(; $dir)? }),
+                workspace: &Directory::new({ "/git/trybuild" $(; $workspace)? }),
                 path_dependencies: &[PathDependency {
                     name: String::from("diesel"),
                     normalized_path: Directory::new("/home/user/documents/rust/diesel/diesel"),
@@ -248,3 +248,24 @@ note: the following traits must be implemented
     | |_^
     = note: this error originates in the macro `anyhow` (in Nightly builds, run with -Z macro-backtrace for more info)
 "}
+
+test_normalize! {test_pyo3_url
+    DIR="/pyo3"
+    WORKSPACE="/pyo3"
+"
+error: `async fn` is not yet supported for Python functions.
+
+Additional crates such as `pyo3-asyncio` can be used to integrate async Rust and Python. For more information, see https://github.com/PyO3/pyo3/issues/1632
+  --> tests/ui/invalid_pyfunctions.rs:10:1
+   |
+10 | async fn async_function() {}
+   | ^^^^^
+" "
+error: `async fn` is not yet supported for Python functions.
+
+Additional crates such as `pyo3-asyncio` can be used to integrate async Rust and Python. For more information, see https://github.com/PyO3/pyo3/issues/1632
+  --> tests/ui/invalid_pyfunctions.rs:10:1
+   |
+10 | async fn async_function() {}
+   | ^^^^^
+"}