Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Ensure .stderr test files always use LF line endings
*.stderr text eol=lf

71 changes: 68 additions & 3 deletions packages/swc-plugin-workflow/transform/tests/errors.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,73 @@
#[cfg(windows)]
use std::fs;
use std::path::PathBuf;
#[cfg(windows)]
use std::sync::Once;
use swc_core::ecma::{
transforms::testing::{FixtureTestConfig, test_fixture},
visit::visit_mut_pass,
};
use swc_workflow::{StepTransform, TransformMode};

// Normalize line endings in stderr files for Windows compatibility
// SWC's error handler on Windows outputs CRLF, but expected files have LF
// We normalize all expected files once at test startup to match Windows output
#[cfg(windows)]
static NORMALIZE_STDERR_FILES: Once = Once::new();

fn normalize_stderr_files() {
#[cfg(windows)]
{
NORMALIZE_STDERR_FILES.call_once(|| {
// Use env! to get the manifest directory at compile time
// This ensures we have the correct path regardless of where the test runs from
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let test_dir = PathBuf::from(manifest_dir).join("tests/errors");
if let Ok(entries) = fs::read_dir(&test_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
// Look for .stderr files in subdirectories
if let Ok(sub_entries) = fs::read_dir(&path) {
for sub_entry in sub_entries.flatten() {
let sub_path = sub_entry.path();
if sub_path.extension().and_then(|s| s.to_str()) == Some("stderr") {
if let Ok(mut content) = fs::read_to_string(&sub_path) {
// On Windows, normalize line endings to LF
// SWC's error handler outputs LF even on Windows
// but Git may check out files with CRLF due to autocrlf
content = content
.replace("\r\n", "\n") // Normalize CRLF to LF
.replace("\r", "\n"); // Handle old Mac-style CR
// Write back with LF line endings
let _ = fs::write(&sub_path, content);
}
}
}
}
}
}
}
Comment on lines +26 to +50
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The normalize_stderr_files() function only normalizes .stderr files one level deep under tests/errors/, but the fixture pattern allows input files at arbitrary nesting depths (e.g., tests/errors/**/input.js). This means .stderr files in deeply nested directories won't be normalized on Windows, causing test failures.

View Details
📝 Patch Details
diff --git a/packages/swc-plugin-workflow/transform/tests/errors.rs b/packages/swc-plugin-workflow/transform/tests/errors.rs
index 01c42d5..016f186 100644
--- a/packages/swc-plugin-workflow/transform/tests/errors.rs
+++ b/packages/swc-plugin-workflow/transform/tests/errors.rs
@@ -23,31 +23,31 @@ fn normalize_stderr_files() {
             // This ensures we have the correct path regardless of where the test runs from
             let manifest_dir = env!("CARGO_MANIFEST_DIR");
             let test_dir = PathBuf::from(manifest_dir).join("tests/errors");
-            if let Ok(entries) = fs::read_dir(&test_dir) {
-                for entry in entries.flatten() {
-                    let path = entry.path();
-                    if path.is_dir() {
-                        // Look for .stderr files in subdirectories
-                        if let Ok(sub_entries) = fs::read_dir(&path) {
-                            for sub_entry in sub_entries.flatten() {
-                                let sub_path = sub_entry.path();
-                                if sub_path.extension().and_then(|s| s.to_str()) == Some("stderr") {
-                                    if let Ok(mut content) = fs::read_to_string(&sub_path) {
-                                        // On Windows, normalize line endings to LF
-                                        // SWC's error handler outputs LF even on Windows
-                                        // but Git may check out files with CRLF due to autocrlf
-                                        content = content
-                                            .replace("\r\n", "\n") // Normalize CRLF to LF
-                                            .replace("\r", "\n"); // Handle old Mac-style CR
-                                        // Write back with LF line endings
-                                        let _ = fs::write(&sub_path, content);
-                                    }
-                                }
+            
+            fn normalize_dir_recursive(dir: &PathBuf) {
+                if let Ok(entries) = fs::read_dir(dir) {
+                    for entry in entries.flatten() {
+                        let path = entry.path();
+                        if path.is_dir() {
+                            // Recursively process subdirectories to handle arbitrary nesting depths
+                            normalize_dir_recursive(&path);
+                        } else if path.extension().and_then(|s| s.to_str()) == Some("stderr") {
+                            if let Ok(mut content) = fs::read_to_string(&path) {
+                                // On Windows, normalize line endings to LF
+                                // SWC's error handler outputs LF even on Windows
+                                // but Git may check out files with CRLF due to autocrlf
+                                content = content
+                                    .replace("\r\n", "\n") // Normalize CRLF to LF
+                                    .replace("\r", "\n"); // Handle old Mac-style CR
+                                // Write back with LF line endings
+                                let _ = fs::write(&path, content);
                             }
                         }
                     }
                 }
             }
+            
+            normalize_dir_recursive(&test_dir);
         });
     }
 }

Analysis

Recursive .stderr file normalization for Windows test compatibility

What fails: The normalize_stderr_files() function in packages/swc-plugin-workflow/transform/tests/errors.rs uses non-recursive directory traversal and only finds .stderr files one level deep (e.g., tests/errors/<category>/*.stderr), while the fixture pattern tests/errors/**/input.js allows test files at arbitrary nesting depths. This causes deeply nested .stderr files to be missed during normalization.

How to reproduce:

  1. Create a deeply nested test structure:

    tests/errors/
      category/
        subcategory/
          input.js
          output-step.stderr
    
  2. On Windows with Git's core.autocrlf=true, the .stderr file is checked out with CRLF line endings (\r\n)

  3. Run the tests - the comparison will fail

Result: The unnormalized CRLF line endings in the deeply nested .stderr file don't match SWC's LF output, causing test failures: actual output (LF) != expected file (CRLF). Tests pass on Unix systems because both have LF.

Expected: The normalize_stderr_files() function should recursively traverse all subdirectories under tests/errors/ to normalize line endings in .stderr files at any nesting depth, matching the behavior of the **/ fixture pattern. This ensures consistent test behavior across Windows and Unix systems regardless of Git's core.autocrlf setting.

Fix applied: Refactored normalize_stderr_files() to use a nested recursive helper function normalize_dir_recursive() that walks the entire directory tree instead of stopping after one level. The fix maintains all existing functionality while handling nested test structures.

});
}
}

#[testing::fixture("tests/errors/**/input.js")]
fn step_mode(input: PathBuf) {
normalize_stderr_files();
let output = input.parent().unwrap().join("output-step.js");
if !output.exists() {
return;
}
test_fixture(
Default::default(),
// The errors occur in any mode, so it doesn't matter
&|_| visit_mut_pass(StepTransform::new(TransformMode::Step, input.file_name().unwrap().to_string_lossy().to_string())),
&|_| {
visit_mut_pass(StepTransform::new(
TransformMode::Step,
input.file_name().unwrap().to_string_lossy().to_string(),
))
},
&input,
&output,
FixtureTestConfig {
Expand All @@ -27,14 +80,20 @@ fn step_mode(input: PathBuf) {

#[testing::fixture("tests/errors/**/input.js")]
fn workflow_mode(input: PathBuf) {
normalize_stderr_files();
let output = input.parent().unwrap().join("output-workflow.js");
if !output.exists() {
return;
}
test_fixture(
Default::default(),
// The errors occur in any mode, so it doesn't matter
&|_| visit_mut_pass(StepTransform::new(TransformMode::Workflow, input.file_name().unwrap().to_string_lossy().to_string())),
&|_| {
visit_mut_pass(StepTransform::new(
TransformMode::Workflow,
input.file_name().unwrap().to_string_lossy().to_string(),
))
},
&input,
&output,
FixtureTestConfig {
Expand All @@ -47,14 +106,20 @@ fn workflow_mode(input: PathBuf) {

#[testing::fixture("tests/errors/**/input.js")]
fn client_mode(input: PathBuf) {
normalize_stderr_files();
let output = input.parent().unwrap().join("output-client.js");
if !output.exists() {
return;
}
test_fixture(
Default::default(),
// The errors occur in any mode, so it doesn't matter
&|_| visit_mut_pass(StepTransform::new(TransformMode::Client, input.file_name().unwrap().to_string_lossy().to_string())),
&|_| {
visit_mut_pass(StepTransform::new(
TransformMode::Client,
input.file_name().unwrap().to_string_lossy().to_string(),
))
},
&input,
&output,
FixtureTestConfig {
Expand Down
Loading