Skip to content
Merged
Changes from 1 commit
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
51 changes: 47 additions & 4 deletions turborepo-tests/helpers/setup_integration_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,55 @@ if [[ "$OSTYPE" == darwin* ]]; then
export TMPDIR=/tmp
fi

# Shared fixture cache to avoid redundant npm installs
# Cache key includes both fixture name and package manager version
# Use TMPDIR for cross-platform compatibility (works on macOS, Linux, Windows/MSYS)
CACHE_BASE_DIR="${TMPDIR:-/tmp}/turbo-fixture-cache"
CACHE_KEY="${FIXTURE_NAME}-${PACKAGE_MANAGER//\//_}" # Replace / with _ for filesystem safety
FIXTURE_CACHE="${CACHE_BASE_DIR}/${CACHE_KEY}"

"${TURBOREPO_TESTS_DIR}/helpers/copy_fixture.sh" "${TARGET_DIR}" "${FIXTURE_NAME}" "${TURBOREPO_TESTS_DIR}/integration/fixtures"
"${TURBOREPO_TESTS_DIR}/helpers/setup_git.sh" "${TARGET_DIR}"
"${TURBOREPO_TESTS_DIR}/helpers/setup_package_manager.sh" "${TARGET_DIR}" "$PACKAGE_MANAGER"
if $INSTALL_DEPS; then
if $INSTALL_DEPS && [ ! -d "$FIXTURE_CACHE" ]; then
Copy link
Contributor

@vercel vercel bot Nov 7, 2025

Choose a reason for hiding this comment

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

Race condition: Multiple parallel test processes can simultaneously check for the fixture cache, find it doesn't exist, and both attempt to create and populate it, causing corruption or conflicts in the shared cache.

View Details
📝 Patch Details
diff --git a/turborepo-tests/helpers/setup_integration_test.sh b/turborepo-tests/helpers/setup_integration_test.sh
index 57e521964..331ecdc86 100755
--- a/turborepo-tests/helpers/setup_integration_test.sh
+++ b/turborepo-tests/helpers/setup_integration_test.sh
@@ -46,25 +46,68 @@ fi
 CACHE_BASE_DIR="${TMPDIR:-/tmp}/turbo-fixture-cache"
 CACHE_KEY="${FIXTURE_NAME}-${PACKAGE_MANAGER//\//_}" # Replace / with _ for filesystem safety
 FIXTURE_CACHE="${CACHE_BASE_DIR}/${CACHE_KEY}"
+FIXTURE_CACHE_LOCK="${CACHE_BASE_DIR}/.${CACHE_KEY}.lock"
 
 if $INSTALL_DEPS && [ ! -d "$FIXTURE_CACHE" ]; then
-  # First test using this fixture+package_manager combo: create cache
-  mkdir -p "$FIXTURE_CACHE"
+  # Use file locking to ensure only one process creates and populates the cache
+  # This prevents race conditions when tests run in parallel (e.g., pytest -n auto)
+  mkdir -p "$CACHE_BASE_DIR"
+  
+  # Check if flock is available for proper locking
+  if command -v flock &> /dev/null; then
+    # Open lock file (create if doesn't exist)
+    exec 200>"$FIXTURE_CACHE_LOCK"
+    
+    # Acquire exclusive lock (wait if another process has it)
+    flock -x 200
+  else
+    # Fallback: use mkdir for atomic lock (less efficient but works on all systems)
+    # mkdir is atomic and will fail if directory already exists
+    lock_acquired=false
+    for attempt in {1..30}; do
+      if mkdir "$FIXTURE_CACHE_LOCK" 2>/dev/null; then
+        lock_acquired=true
+        break
+      fi
+      sleep 0.1
+    done
+    if [ "$lock_acquired" = false ]; then
+      # Couldn't acquire lock after timeout, but cache might exist now
+      if [ -d "$FIXTURE_CACHE" ]; then
+        exit 0
+      fi
+      echo "Error: Could not acquire fixture cache lock after timeout" >&2
+      exit 1
+    fi
+  fi
+  
+  # Double-check: another process might have created the cache while we waited
+  if [ ! -d "$FIXTURE_CACHE" ]; then
+    # First test using this fixture+package_manager combo: create cache
+    mkdir -p "$FIXTURE_CACHE"
 
-  # Copy fixture to cache location
-  "${TURBOREPO_TESTS_DIR}/helpers/copy_fixture.sh" "${FIXTURE_CACHE}" "${FIXTURE_NAME}" "${TURBOREPO_TESTS_DIR}/integration/fixtures"
+    # Copy fixture to cache location
+    "${TURBOREPO_TESTS_DIR}/helpers/copy_fixture.sh" "${FIXTURE_CACHE}" "${FIXTURE_NAME}" "${TURBOREPO_TESTS_DIR}/integration/fixtures"
 
-  # Setup git in cache (needed for install_deps.sh to commit)
-  "${TURBOREPO_TESTS_DIR}/helpers/setup_git.sh" "${FIXTURE_CACHE}"
+    # Setup git in cache (needed for install_deps.sh to commit)
+    "${TURBOREPO_TESTS_DIR}/helpers/setup_git.sh" "${FIXTURE_CACHE}"
 
-  # Setup package manager and install deps in cache
-  "${TURBOREPO_TESTS_DIR}/helpers/setup_package_manager.sh" "${FIXTURE_CACHE}" "$PACKAGE_MANAGER"
-  cd "${FIXTURE_CACHE}"
-  "${TURBOREPO_TESTS_DIR}/helpers/install_deps.sh" "$PACKAGE_MANAGER"
-  cd - > /dev/null
+    # Setup package manager and install deps in cache
+    "${TURBOREPO_TESTS_DIR}/helpers/setup_package_manager.sh" "${FIXTURE_CACHE}" "$PACKAGE_MANAGER"
+    cd "${FIXTURE_CACHE}"
+    "${TURBOREPO_TESTS_DIR}/helpers/install_deps.sh" "$PACKAGE_MANAGER"
+    cd - > /dev/null
 
-  # Remove .git from cache since each test needs its own git repo
-  rm -rf "${FIXTURE_CACHE}/.git"
+    # Remove .git from cache since each test needs its own git repo
+    rm -rf "${FIXTURE_CACHE}/.git"
+  fi
+  
+  # Release lock
+  if command -v flock &> /dev/null; then
+    flock -u 200
+  else
+    rmdir "$FIXTURE_CACHE_LOCK" 2>/dev/null || true
+  fi
 fi
 
 if $INSTALL_DEPS && [ -d "$FIXTURE_CACHE" ]; then

Analysis

Race condition in fixture cache creation during parallel test execution

What fails: When multiple test processes run in parallel (using pytest -n auto or similar), they can simultaneously check if the shared fixture cache exists, both find it missing, and proceed to create and populate it concurrently. This causes file conflicts during cache setup and corrupts the shared node_modules cache.

How to reproduce:

cd turborepo-tests/integration
npm run pretest:parallel
npm run test:parallel

With timing conditions, multiple concurrent tests will detect the same fixture cache (/tmp/turbo-fixture-cache/[email protected]) doesn't exist and attempt simultaneous:

  • mkdir -p and copy_fixture.sh operations on the same directory
  • Concurrent npm install in the same cache directory

Result: Incomplete or corrupted node_modules, failed dependency installations, and test failures that are difficult to reproduce (timing-dependent).

Expected behavior: Only the first process should create and populate the cache; others should wait for it to complete before using it.

Solution implemented: Added file-level locking using:

  • flock when available (Linux/systems with util-linux)
  • Atomic mkdir for lock directory when flock is unavailable (macOS compatibility)

This ensures only one process at a time creates and populates each shared fixture cache.

# First test using this fixture+package_manager combo: create cache
mkdir -p "$FIXTURE_CACHE"

# Copy fixture to cache location
"${TURBOREPO_TESTS_DIR}/helpers/copy_fixture.sh" "${FIXTURE_CACHE}" "${FIXTURE_NAME}" "${TURBOREPO_TESTS_DIR}/integration/fixtures"

# Setup git in cache (needed for install_deps.sh to commit)
"${TURBOREPO_TESTS_DIR}/helpers/setup_git.sh" "${FIXTURE_CACHE}"

# Setup package manager and install deps in cache
"${TURBOREPO_TESTS_DIR}/helpers/setup_package_manager.sh" "${FIXTURE_CACHE}" "$PACKAGE_MANAGER"
cd "${FIXTURE_CACHE}"
"${TURBOREPO_TESTS_DIR}/helpers/install_deps.sh" "$PACKAGE_MANAGER"
cd - > /dev/null

# Remove .git from cache since each test needs its own git repo
rm -rf "${FIXTURE_CACHE}/.git"
fi

if $INSTALL_DEPS && [ -d "$FIXTURE_CACHE" ]; then
# Use cached fixture with pre-installed dependencies
cp -a "${FIXTURE_CACHE}/." "${TARGET_DIR}/"

# Setup fresh git repo for this test
"${TURBOREPO_TESTS_DIR}/helpers/setup_git.sh" "${TARGET_DIR}"

# Commit the already-installed dependencies
cd "${TARGET_DIR}"
git add .
if [[ $(git status --porcelain) ]]; then
git commit -am "Install dependencies" --quiet > /dev/null 2>&1 || true
fi
cd - > /dev/null
else
# No caching: use original flow
"${TURBOREPO_TESTS_DIR}/helpers/copy_fixture.sh" "${TARGET_DIR}" "${FIXTURE_NAME}" "${TURBOREPO_TESTS_DIR}/integration/fixtures"
"${TURBOREPO_TESTS_DIR}/helpers/setup_git.sh" "${TARGET_DIR}"
"${TURBOREPO_TESTS_DIR}/helpers/setup_package_manager.sh" "${TARGET_DIR}" "$PACKAGE_MANAGER"
if $INSTALL_DEPS; then
"${TURBOREPO_TESTS_DIR}/helpers/install_deps.sh" "$PACKAGE_MANAGER"
fi
fi

# Set TURBO env var, it is used by tests to run the binary
Expand Down
Loading