Skip to content

Commit 6452141

Browse files
jacalataclaude
andcommitted
Address review feedback on e2e test suite
- test_workbook_permissions: _unique() the username to avoid 409 on second run - pyproject.toml: don't leak -n auto into test_e2e/ runs (they're xdist-incompat) - test_datasources_get: migrate to filter(name=...) to fix the known flake - test_pager_datasources_returns_items: loosen strict == to >= to survive concurrent publishers - test_projects_get_by_path: _unique() the three fixed project names - Migrate bare except in test_workbook_permissions to warnings.warn - Prefer TSC.Resource over the private-path import Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 866049f commit 6452141

8 files changed

Lines changed: 65 additions & 54 deletions

‎test_e2e/_helpers.py‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"""Shared helpers for e2e tests."""
2+
3+
import uuid
4+
5+
6+
def _unique(prefix: str) -> str:
7+
"""Generate a name unique to this test run.
8+
9+
Every user/group/project name used by an e2e test must be per-run to
10+
avoid 409 conflicts when: (a) two runs execute against the same site
11+
in parallel, or (b) a prior run crashed after `create` but before the
12+
`finally` block removed it.
13+
"""
14+
return f"{prefix}-{uuid.uuid4().hex[:8]}"

‎test_e2e/conftest.py‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,20 @@ def pytest_configure(config):
99
config.addinivalue_line("markers", "e2e_admin: mark test as end-to-end requiring SiteAdmin credentials")
1010

1111

12+
def pytest_collection_modifyitems(config, items):
13+
"""Force sequential execution for e2e tests.
14+
15+
The project-level pytest addopts includes ``-n auto`` (pytest-xdist),
16+
which parallelises test execution. Our e2e tests share module-scoped
17+
server fixtures and mutate the same live site, so they must run
18+
sequentially to avoid races (409 conflicts, cleanup ordering, etc.).
19+
"""
20+
if getattr(config.option, "numprocesses", None) not in (None, 0):
21+
config.option.numprocesses = 0
22+
if getattr(config.option, "dist", None) not in (None, "no"):
23+
config.option.dist = "no"
24+
25+
1226
def _http_options() -> dict:
1327
verify = os.environ.get("TABLEAU_VERIFY_SSL", "true").lower() != "false"
1428
if not verify:

‎test_e2e/test_datasources_crud.py‎

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,11 @@ def test_datasource_publish(server, datasource):
3636

3737

3838
def test_datasources_get(server, datasource):
39-
"""datasources.get() with a name filter returns the published datasource."""
40-
opts = TSC.RequestOptions()
41-
opts.filter.add(
42-
TSC.Filter(TSC.RequestOptions.Field.Name, TSC.RequestOptions.Operator.Equals, "tsc-e2e-datasource-crud")
39+
"""datasources.filter() by name returns the published datasource."""
40+
results = list(server.datasources.filter(name="tsc-e2e-datasource-crud"))
41+
assert any(ds.id == datasource.id for ds in results), (
42+
f"Published datasource {datasource.id!r} not found in filter results: " f"{[ds.id for ds in results]}"
4343
)
44-
results, pagination = server.datasources.get(opts)
45-
assert len(results) >= 1
46-
assert any(ds.id == datasource.id for ds in results)
4744

4845

4946
def test_datasources_get_by_id(server, datasource):

‎test_e2e/test_favorites.py‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010

1111
import pytest
1212
import tableauserverclient as TSC
13-
from tableauserverclient.models import Resource
1413

1514
ASSETS_DIR = Path(__file__).parent / "assets"
1615
SAMPLE_WORKBOOK = ASSETS_DIR / "WorkbookWithoutExtract.twbx"
@@ -43,7 +42,7 @@ def test_favorites_workbook(server, workbook):
4342
"""A workbook can be added to and removed from favorites."""
4443
user = TSC.UserItem()
4544
user.id = server.user_id
46-
server.favorites.add_favorite(user, Resource.Workbook, workbook)
45+
server.favorites.add_favorite(user, TSC.Resource.Workbook, workbook)
4746
server.favorites.get(user)
4847
try:
4948
assert any(f.id == workbook.id for f in user.favorites.get("workbooks", []))

‎test_e2e/test_pagination.py‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,16 +59,19 @@ def test_pager_workbooks_count_matches_get(server, workbooks_for_pagination):
5959

6060

6161
def test_pager_datasources_returns_items(server, datasource_for_pagination):
62-
"""TSC.Pager iterates all datasources; count matches total_available."""
62+
"""TSC.Pager iterates all datasources; count is at least the fixture datasource.
63+
64+
Uses ``>=`` rather than ``== total_declared`` because concurrent publishers on
65+
the same site can add or remove datasources between the ``get()`` count call
66+
and the ``Pager`` iteration, causing flakes on shared test sites.
67+
"""
6368
_, pagination_item = server.datasources.get()
6469
total_declared = pagination_item.total_available
6570
assert total_declared > 0, "Server has no datasources — fixture likely failed"
6671

6772
pager_count = sum(1 for _ in TSC.Pager(server.datasources))
6873

69-
assert (
70-
pager_count == total_declared
71-
), f"Pager yielded {pager_count} datasources but server reported {total_declared}"
74+
assert pager_count >= 1, f"Pager yielded {pager_count} datasources but expected at least the fixture datasource"
7275

7376

7477
def test_queryset_all_workbooks_matches_pager(server, workbooks_for_pagination):

‎test_e2e/test_projects_get_by_path.py‎

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,12 @@
1010
TABLEAU_SITEADMIN_TOKEN=... TABLEAU_SITEADMIN_TOKEN_NAME=... \
1111
pytest test_e2e/test_projects_get_by_path.py -m e2e_admin -v
1212
"""
13+
1314
import pytest
1415
import tableauserverclient as TSC
1516

17+
from test_e2e._helpers import _unique
18+
1619
# pytestmark is intentionally NOT set at module level because this file mixes
1720
# @pytest.mark.e2e (publisher credentials) and @pytest.mark.e2e_admin (site-admin
1821
# credentials) tests. A module-wide pytestmark would apply the same mark to every
@@ -43,8 +46,7 @@ def test_get_by_path_strips_leading_trailing_slashes(server, default_project):
4346
result = server.projects.get_by_path("/Default/")
4447
assert result is not None, "Expected to find project via path '/Default/', got None"
4548
assert result.id == default_project.id, (
46-
f"Path '/Default/' resolved to project {result.id!r} "
47-
f"but expected {default_project.id!r}"
49+
f"Path '/Default/' resolved to project {result.id!r} " f"but expected {default_project.id!r}"
4850
)
4951

5052

@@ -58,27 +60,21 @@ def test_get_by_path_returns_none_for_missing_project(server):
5860
@pytest.mark.e2e_admin
5961
def test_get_child_project_by_path(server_admin):
6062
"""get_by_path('Parent/Child') resolves to the correct nested child project."""
61-
parent_name = "tsc-e2e-gbp-parent"
62-
child_name = "tsc-e2e-gbp-child"
63+
parent_name = _unique("tsc-e2e-gbp-parent")
64+
child_name = _unique("tsc-e2e-gbp-child")
6365
parent = None
6466
child = None
6567
try:
6668
parent = server_admin.projects.create(TSC.ProjectItem(name=parent_name))
67-
child = server_admin.projects.create(
68-
TSC.ProjectItem(name=child_name, parent_id=parent.id)
69-
)
69+
child = server_admin.projects.create(TSC.ProjectItem(name=child_name, parent_id=parent.id))
7070

7171
result = server_admin.projects.get_by_path(f"{parent_name}/{child_name}")
7272

73-
assert result is not None, (
74-
f"Expected to find child project at path '{parent_name}/{child_name}', got None"
75-
)
76-
assert result.id == child.id, (
77-
f"get_by_path returned project {result.id!r} but expected child {child.id!r}"
78-
)
79-
assert result.parent_id == parent.id, (
80-
f"Child project parent_id {result.parent_id!r} does not match parent {parent.id!r}"
81-
)
73+
assert result is not None, f"Expected to find child project at path '{parent_name}/{child_name}', got None"
74+
assert result.id == child.id, f"get_by_path returned project {result.id!r} but expected child {child.id!r}"
75+
assert (
76+
result.parent_id == parent.id
77+
), f"Child project parent_id {result.parent_id!r} does not match parent {parent.id!r}"
8278
finally:
8379
if child is not None:
8480
server_admin.projects.delete(child.id)
@@ -89,17 +85,13 @@ def test_get_child_project_by_path(server_admin):
8985
@pytest.mark.e2e_admin
9086
def test_get_by_path_with_spaces_in_name(server_admin):
9187
"""get_by_path works correctly when the project name contains spaces."""
92-
project_name = "TSC E2E Spaced Project"
88+
project_name = _unique("TSC E2E Spaced Project")
9389
project = None
9490
try:
9591
project = server_admin.projects.create(TSC.ProjectItem(name=project_name))
9692
result = server_admin.projects.get_by_path(project_name)
97-
assert result is not None, (
98-
f"Expected to find project '{project_name}' by path, got None"
99-
)
100-
assert result.name == project_name, (
101-
f"Project name mismatch: expected {project_name!r}, got {result.name!r}"
102-
)
93+
assert result is not None, f"Expected to find project '{project_name}' by path, got None"
94+
assert result.name == project_name, f"Project name mismatch: expected {project_name!r}, got {result.name!r}"
10395
assert result.id == project.id
10496
finally:
10597
if project is not None:

‎test_e2e/test_users_groups.py‎

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,12 @@
88
pytest test_e2e/test_users_groups.py -v
99
"""
1010

11-
import uuid
12-
1311
import pytest
1412
import tableauserverclient as TSC
1513

16-
pytestmark = pytest.mark.e2e_admin
17-
14+
from test_e2e._helpers import _unique
1815

19-
def _unique(prefix: str) -> str:
20-
"""Generate a name unique to this test run.
21-
22-
Every user/group name used by an e2e test must be per-run to avoid
23-
409 conflicts when: (a) two runs execute against the same site in
24-
parallel, or (b) a prior run crashed after `create` but before the
25-
`finally` block removed it.
26-
"""
27-
return f"{prefix}-{uuid.uuid4().hex[:8]}"
16+
pytestmark = pytest.mark.e2e_admin
2817

2918

3019
def test_users_get_returns_nonempty_list(server_admin):

‎test_e2e/test_workbook_permissions.py‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,14 @@
77
pytest test_e2e/test_workbook_permissions.py -v
88
"""
99

10+
import warnings
1011
from pathlib import Path
1112

1213
import pytest
1314
import tableauserverclient as TSC
1415

16+
from test_e2e._helpers import _unique
17+
1518
ASSETS_DIR = Path(__file__).parent / "assets"
1619
SAMPLE_WORKBOOK = ASSETS_DIR / "WorkbookWithoutExtract.twbx"
1720

@@ -25,7 +28,7 @@ def workbook_and_user(server_admin, default_project):
2528
wb = server_admin.workbooks.publish(wb, SAMPLE_WORKBOOK, TSC.Server.PublishMode.Overwrite)
2629

2730
try:
28-
user = TSC.UserItem("tsc-e2e-perm-testuser", TSC.UserItem.Roles.Viewer)
31+
user = TSC.UserItem(_unique("tsc-e2e-perm-testuser"), TSC.UserItem.Roles.Viewer)
2932
user = server_admin.users.add(user)
3033
except Exception:
3134
server_admin.workbooks.delete(wb.id)
@@ -78,8 +81,8 @@ def test_update_permissions_appears_on_populate(server_admin, workbook_and_user)
7881
)
7982
try:
8083
server_admin.workbooks.delete_permission(workbook, delete_rule)
81-
except Exception:
82-
pass # Rule may already be deleted by the test body
84+
except Exception as exc:
85+
warnings.warn(f"Cleanup delete_permission failed (rule may already be deleted): {exc}")
8386

8487

8588
def test_view_populate_permissions_returns_list(server_admin, workbook_and_user):
@@ -139,5 +142,5 @@ def test_delete_permission_removes_rule(server_admin, workbook_and_user):
139142
)
140143
try:
141144
server_admin.workbooks.delete_permission(workbook, delete_rule)
142-
except Exception:
143-
pass # Rule may already be deleted by the test body
145+
except Exception as exc:
146+
warnings.warn(f"Cleanup delete_permission failed (rule may already be deleted): {exc}")

0 commit comments

Comments
 (0)