Skip to content

Commit 3a825cc

Browse files
test(iast): give iast_packages fixtures private filesystem state (#19663)
APPSEC-69623 These flask endpoints are hit by both `test_packages_patched` and `test_packages_not_patched`, which run in different xdist workers sharing a cwd and a HOME. A fixed path lets one request's cleanup delete state another request is still using. **Fixes the flake** (7 quarantined tests, top impact on main). `pkg_virtualenv` built the env at `os.getcwd()/<name>`, so a concurrent `rmtree` broke creation: `[Errno 2] No such file or directory: .../myenv/lib/python3.12/site-packages/pip/_vendor/pygments/formatters`. Now a per-request `TemporaryDirectory`. **Same bug, not yet caught.** `pkg_importlib_resources`, `pkg_platformdirs`, `pkg_openpyxl`, `pkg_pandas`, `pkg_zipp`, `pkg_iniconfig` shared `data/`, `~/.local/share/<app>`, `example.xlsx`, `example.csv`, `example.zip`, `example.ini`. None have failed in 30 days — they write in milliseconds, where a venv takes ~7s — but the race is the same. Notes: - `pkg_importlib_resources` stays under the cwd so `resources.files()` can resolve it, hence the cache invalidation and the `sys.modules` cleanup. - `pkg_platformdirs` can't be relocated (the expected output embeds the path), so it uses `exist_ok` and tolerates a concurrent removal. - `pkg_zipp` now reports member names instead of full paths, so the assertion no longer depends on the archive's location; expected string updated. Reviewed by codex; findings applied. Verified via attempt-to-fix on the 7 keys. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: christophe.papazian <christophe.papazian@datadoghq.com>
1 parent 8d8bc49 commit 3a825cc

8 files changed

Lines changed: 90 additions & 82 deletions

File tree

tests/appsec/iast_packages/packages/pkg_importlib_resources.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@
44
https://pypi.org/project/importlib-resources/
55
"""
66

7+
import importlib
78
import os
89
import shutil
10+
import sys
11+
import uuid
912

1013
from flask import Blueprint
1114
from flask import request
@@ -25,15 +28,16 @@ def pkg_importlib_resources_view():
2528
try:
2629
resource_name = request.args.get("package_param", "default.txt")
2730

28-
# Ensure the data directory and file exist
29-
data_dir = "data"
31+
# Unique per request: xdist workers share a cwd, so under a fixed name one request's
32+
# cleanup deletes the file another is still reading. It has to stay under the cwd for
33+
# resources.files() to resolve it as a namespace package, hence the cache invalidation.
34+
data_dir = f"data_{uuid.uuid4().hex}"
3035
file_path = os.path.join(data_dir, resource_name)
3136

32-
if not os.path.exists(data_dir):
33-
os.makedirs(data_dir)
34-
if not os.path.exists(file_path):
35-
with open(file_path, "w") as f:
36-
f.write("This is the default content of the file.")
37+
os.makedirs(data_dir, exist_ok=True)
38+
with open(file_path, "w") as f:
39+
f.write("This is the default content of the file.")
40+
importlib.invalidate_caches()
3741

3842
try:
3943
content = resources.files(data_dir).joinpath(resource_name).read_text()
@@ -45,10 +49,9 @@ def pkg_importlib_resources_view():
4549
except Exception as e:
4650
response.result1 = f"Error: {str(e)}"
4751
finally:
48-
if data_dir and os.path.exists(data_dir):
49-
try:
50-
shutil.rmtree(data_dir)
51-
except Exception:
52-
pass
52+
if data_dir:
53+
# resources.files() leaves a namespace module behind for each unique name.
54+
sys.modules.pop(data_dir, None)
55+
shutil.rmtree(data_dir, ignore_errors=True)
5356

5457
return response.json()

tests/appsec/iast_packages/packages/pkg_iniconfig.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"""
66

77
import os
8+
import tempfile
89

910
from flask import Blueprint
1011
from flask import jsonify
@@ -25,20 +26,21 @@ def pkg_iniconfig_view():
2526
try:
2627
value = request.args.get("package_param", "test1234")
2728
ini_content = f"[section]\nkey={value}"
28-
ini_path = "example.ini"
2929

30-
try:
31-
with open(ini_path, "w") as f:
32-
f.write(ini_content)
30+
# Private directory per request: xdist workers share a cwd, so under a fixed name one
31+
# request's cleanup removes the file another request is still reading.
32+
with tempfile.TemporaryDirectory() as tmp_dir:
33+
ini_path = os.path.join(tmp_dir, "example.ini")
3334

34-
config = iniconfig.IniConfig(ini_path)
35-
parsed_data = {section.name: list(section.items()) for section in config}
36-
result_output = f"Parsed INI data: {parsed_data}"
35+
try:
36+
with open(ini_path, "w") as f:
37+
f.write(ini_content)
3738

38-
if os.path.exists(ini_path):
39-
os.remove(ini_path)
40-
except Exception as e:
41-
result_output = f"Error: {str(e)}"
39+
config = iniconfig.IniConfig(ini_path)
40+
parsed_data = {section.name: list(section.items()) for section in config}
41+
result_output = f"Parsed INI data: {parsed_data}"
42+
except Exception as e:
43+
result_output = f"Error: {str(e)}"
4244

4345
response.result1 = result_output
4446
except Exception as e:

tests/appsec/iast_packages/packages/pkg_openpyxl.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"""
66

77
import os
8+
import tempfile
89

910
from flask import Blueprint
1011
from flask import request
@@ -31,17 +32,16 @@ def pkg_openpyxl_view():
3132
# Write the parameter value to the first cell
3233
ws["A1"] = param_value
3334

34-
# Save the workbook to a file
35-
file_path = "example.xlsx"
36-
wb.save(file_path)
35+
# Private directory per request: xdist workers share a cwd, so under a fixed name one
36+
# request's cleanup removes the file another request is still reading.
37+
with tempfile.TemporaryDirectory() as tmp_dir:
38+
file_path = os.path.join(tmp_dir, "example.xlsx")
39+
wb.save(file_path)
3740

38-
# Read back the value from the file to ensure it was written correctly
39-
wb_read = openpyxl.load_workbook(file_path)
40-
ws_read = wb_read.active
41-
read_value = ws_read["A1"].value
42-
43-
# Clean up the created file
44-
os.remove(file_path)
41+
# Read back the value from the file to ensure it was written correctly
42+
wb_read = openpyxl.load_workbook(file_path)
43+
ws_read = wb_read.active
44+
read_value = ws_read["A1"].value
4545

4646
result_output = f"Written value: {read_value}"
4747

tests/appsec/iast_packages/packages/pkg_pandas.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"""
66

77
import os
8+
import tempfile
89

910
from flask import Blueprint
1011
from flask import request
@@ -27,16 +28,15 @@ def pkg_pandas_view():
2728
# Create a DataFrame
2829
df = pd.DataFrame({"Column1": [param_value]})
2930

30-
# Save the DataFrame to a CSV file
31-
file_path = "example.csv"
32-
df.to_csv(file_path, index=False)
31+
# Private directory per request: xdist workers share a cwd, so under a fixed name one
32+
# request's cleanup removes the file another request is still reading.
33+
with tempfile.TemporaryDirectory() as tmp_dir:
34+
file_path = os.path.join(tmp_dir, "example.csv")
35+
df.to_csv(file_path, index=False)
3336

34-
# Read back the value from the file to ensure it was written correctly
35-
df_read = pd.read_csv(file_path)
36-
read_value = df_read.iloc[0]["Column1"]
37-
38-
# Clean up the created file
39-
os.remove(file_path)
37+
# Read back the value from the file to ensure it was written correctly
38+
df_read = pd.read_csv(file_path)
39+
read_value = df_read.iloc[0]["Column1"]
4040

4141
result_output = f"Written value: {read_value}"
4242

tests/appsec/iast_packages/packages/pkg_platformdirs.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
https://pypi.org/project/platformdirs/
55
"""
66

7+
import contextlib
78
import os
89

910
from flask import Blueprint
@@ -27,14 +28,14 @@ def pkg_platformdirs_view():
2728
# Get the user data directory for the application
2829
data_dir = user_data_dir(app_name)
2930

30-
# Create the directory if it doesn't exist
31-
if not os.path.exists(data_dir):
32-
os.makedirs(data_dir)
31+
# The path derives from the app name, so every xdist worker shares it and two requests
32+
# can create and remove it concurrently.
33+
os.makedirs(data_dir, exist_ok=True)
3334

3435
result_output = f"User data directory for {app_name}: {data_dir}"
3536

36-
# Clean up the created directory
37-
if os.path.exists(data_dir):
37+
# Clean up the created directory; another worker may have removed it already.
38+
with contextlib.suppress(FileNotFoundError):
3839
os.rmdir(data_dir)
3940

4041
response.result1 = result_output

tests/appsec/iast_packages/packages/pkg_virtualenv.py

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"""
66

77
import os
8-
import shutil
8+
import tempfile
99

1010
from flask import Blueprint
1111
from flask import request
@@ -24,24 +24,23 @@ def pkg_virtualenv_view():
2424

2525
try:
2626
env_name = request.args.get("package_param", "default-env")
27-
env_path = os.path.join(os.getcwd(), env_name)
28-
29-
try:
30-
# Create a virtual environment
31-
virtualenv.cli_run([env_path])
32-
result_output = "Virtual environment created at replaced_path"
33-
34-
# Optionally, list the contents of the virtual environment's bin/Scripts directory
35-
_ = os.path.join(env_path, "bin" if os.name != "nt" else "Scripts")
36-
result_output += "\nContents of replaced_path: replaced_contents"
37-
38-
except Exception as e:
39-
result_output = f"Error: {str(e)}"
40-
41-
finally:
42-
# Clean up the created virtual environment
43-
if os.path.exists(env_path):
44-
shutil.rmtree(env_path)
27+
# Private directory per request: xdist workers share a cwd, so under a fixed path one
28+
# request's cleanup deletes the environment another request is still creating.
29+
with tempfile.TemporaryDirectory() as tmp_dir:
30+
# basename so the env cannot land outside tmp_dir and escape its cleanup.
31+
env_path = os.path.join(tmp_dir, os.path.basename(env_name) or "env")
32+
33+
try:
34+
# Create a virtual environment
35+
virtualenv.cli_run([env_path])
36+
result_output = "Virtual environment created at replaced_path"
37+
38+
# Optionally, list the contents of the virtual environment's bin/Scripts directory
39+
_ = os.path.join(env_path, "bin" if os.name != "nt" else "Scripts")
40+
result_output += "\nContents of replaced_path: replaced_contents"
41+
42+
except Exception as e:
43+
result_output = f"Error: {str(e)}"
4544

4645
response.result1 = result_output
4746
except Exception as e:

tests/appsec/iast_packages/packages/pkg_zipp.py

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"""
66

77
import os
8+
import tempfile
89
import zipfile
910

1011
from flask import Blueprint
@@ -25,21 +26,23 @@ def pkg_zipp_view():
2526
try:
2627
zip_param = request.args.get("package_param", "example.zip")
2728

28-
try:
29-
# Create an example zip file
30-
with zipfile.ZipFile(zip_param, "w") as zip_file:
31-
zip_file.writestr("example.txt", "This is an example file.")
32-
33-
# Read the contents of the zip file using zipp
34-
zip_path = zipp.Path(zip_param)
35-
contents = [str(file) for file in zip_path.iterdir()]
36-
result_output = f"Contents of {zip_param}: {contents}"
37-
38-
# Clean up the created zip file
39-
if os.path.exists(zip_param):
40-
os.remove(zip_param)
41-
except Exception as e:
42-
result_output = f"Error: {str(e)}"
29+
# Private directory per request: xdist workers share a cwd, so under a fixed name one
30+
# request's cleanup removes the archive another request is still reading.
31+
with tempfile.TemporaryDirectory() as tmp_dir:
32+
zip_file_path = os.path.join(tmp_dir, os.path.basename(zip_param))
33+
34+
try:
35+
# Create an example zip file
36+
with zipfile.ZipFile(zip_file_path, "w") as zip_file:
37+
zip_file.writestr("example.txt", "This is an example file.")
38+
39+
# Read the contents of the zip file using zipp. Report member names rather than
40+
# full paths so the result does not depend on where the archive lives.
41+
zip_path = zipp.Path(zip_file_path)
42+
contents = [file.name for file in zip_path.iterdir()]
43+
result_output = f"Contents of {os.path.basename(zip_file_path)}: {contents}"
44+
except Exception as e:
45+
result_output = f"Error: {str(e)}"
4346

4447
response.result1 = result_output
4548
except Exception as e:

tests/appsec/iast_packages/test_packages.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -725,7 +725,7 @@ def create_venv(self):
725725
"zipp",
726726
"3.18.2",
727727
"example.zip",
728-
"Contents of example.zip: ['example.zip/example.txt']",
728+
"Contents of example.zip: ['example.txt']",
729729
"",
730730
),
731731
## Skip due to typing-extensions added to the denylist

0 commit comments

Comments
 (0)