Skip to content

Commit 6f4a7e7

Browse files
committed
Update E2E Tests v3
* Update E2E Tests v3
1 parent de8a727 commit 6f4a7e7

19 files changed

Lines changed: 239 additions & 80 deletions

ci/e2e/framework/utils.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ def ok(self) -> bool:
2727
STARTUP_DISCOVERY_FUNCTION_MARKERS = (
2828
"Calling Function: syncEngine.getDefaultRootDetails()",
2929
"Calling Function: syncEngine.getDefaultDriveDetails()",
30+
"Calling Function: syncEngine.fetchRealOnlineDriveIdentifier()",
3031
)
3132
STARTUP_TRANSIENT_HTTP_MARKERS = (
3233
"HTTP request returned status code 403 (Forbidden)",

ci/e2e/testcases/monitor_case_base.py

Lines changed: 104 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,58 @@ def _read_stdout_from_offset(self, stdout_file: Path, start_offset: int) -> str:
6868
return ""
6969
return content[start_offset:]
7070

71+
def _read_app_logs(self, app_log_dir: Path) -> str:
72+
if not app_log_dir.exists():
73+
return ""
74+
75+
segments: list[str] = []
76+
for log_file in sorted(path for path in app_log_dir.rglob("*.log") if path.is_file()):
77+
try:
78+
segments.append(log_file.read_text(encoding="utf-8", errors="replace"))
79+
except OSError:
80+
continue
81+
return "\n".join(segments)
82+
83+
def _read_app_logs_from_offset(self, app_log_dir: Path, start_offset: int) -> str:
84+
content = self._read_app_logs(app_log_dir)
85+
if start_offset <= 0:
86+
return content
87+
if start_offset >= len(content):
88+
return ""
89+
return content[start_offset:]
90+
91+
def _monitor_app_log_dir_for_stdout(self, stdout_file: Path) -> Path:
92+
return stdout_file.parent / "app-logs"
93+
94+
def _remember_monitor_app_log_offset(self, stdout_file: Path, offset: int) -> None:
95+
if not hasattr(self, "_monitor_app_log_start_offsets"):
96+
self._monitor_app_log_start_offsets = {}
97+
self._monitor_app_log_start_offsets[str(stdout_file)] = offset
98+
99+
def _monitor_app_log_start_offset(self, stdout_file: Path) -> int:
100+
offsets = getattr(self, "_monitor_app_log_start_offsets", {})
101+
return int(offsets.get(str(stdout_file), 0))
102+
103+
def _read_monitor_output_from_offsets(self, stdout_file: Path, stdout_start_offset: int) -> str:
104+
"""Read post-mutation monitor evidence from stdout and the app log.
105+
106+
Local monitor event processing does not always emit another global
107+
sync-complete marker after an inotify wake. The stable evidence is the
108+
per-event upload/delete/move output, and that may be present in stdout,
109+
the configured application log, or both depending on verbosity and CI
110+
buffering. Offset both streams so assertions only inspect activity that
111+
happened after the test mutation.
112+
"""
113+
stdout_segment = self._read_stdout_from_offset(stdout_file, stdout_start_offset)
114+
app_log_dir = self._monitor_app_log_dir_for_stdout(stdout_file)
115+
app_log_segment = self._read_app_logs_from_offset(
116+
app_log_dir,
117+
self._monitor_app_log_start_offset(stdout_file),
118+
)
119+
if stdout_segment and app_log_segment:
120+
return stdout_segment + "\n" + app_log_segment
121+
return stdout_segment or app_log_segment
122+
71123
def _wait_for_initial_sync_complete(
72124
self,
73125
stdout_file: Path,
@@ -130,17 +182,21 @@ def _prepare_monitor_for_local_mutation(
130182
quiet_seconds: float = 3.0,
131183
timeout_seconds: int = 30,
132184
) -> int:
133-
"""Wait for monitor readiness and return the post-mutation log offset."""
185+
"""Wait for monitor readiness and return the post-mutation stdout offset."""
134186
ready = self._wait_for_monitor_stdout_quiet(
135187
process,
136188
stdout_file,
137189
quiet_seconds=quiet_seconds,
138190
timeout_seconds=timeout_seconds,
139191
)
140192
content = self._read_stdout(stdout_file)
193+
app_log_content = self._read_app_logs(self._monitor_app_log_dir_for_stdout(stdout_file))
194+
self._remember_monitor_app_log_offset(stdout_file, len(app_log_content))
141195
details["monitor_ready_after_initial_sync"] = ready
142196
details["initial_sync_complete_count_before_mutation"] = content.count(self.SYNC_COMPLETE_PATTERN)
197+
details["app_log_sync_complete_count_before_mutation"] = app_log_content.count(self.SYNC_COMPLETE_PATTERN)
143198
details["mutation_log_start_offset"] = len(content)
199+
details["mutation_app_log_start_offset"] = len(app_log_content)
144200
return len(content)
145201

146202
def _wait_for_monitor_patterns(
@@ -154,7 +210,7 @@ def _wait_for_monitor_patterns(
154210
deadline = time.time() + timeout_seconds
155211

156212
while time.time() < deadline:
157-
content = self._read_stdout_from_offset(stdout_file, start_offset)
213+
content = self._read_monitor_output_from_offsets(stdout_file, start_offset)
158214
if all(pattern in content for pattern in required_patterns):
159215
return True
160216
time.sleep(poll_interval)
@@ -174,7 +230,7 @@ def _wait_for_stdout_growth_patterns(
174230
latest_segment = ""
175231

176232
while time.time() < deadline:
177-
latest_segment = self._read_stdout_from_offset(stdout_file, start_offset)
233+
latest_segment = self._read_monitor_output_from_offsets(stdout_file, start_offset)
178234
if all(pattern in latest_segment for pattern in required_patterns):
179235
return True, latest_segment
180236
time.sleep(poll_interval)
@@ -192,7 +248,7 @@ def _wait_for_any_monitor_pattern_group(
192248
deadline = time.time() + timeout_seconds
193249

194250
while time.time() < deadline:
195-
content = self._read_stdout_from_offset(stdout_file, start_offset)
251+
content = self._read_monitor_output_from_offsets(stdout_file, start_offset)
196252
for idx, group in enumerate(alternative_pattern_groups):
197253
if all(pattern in content for pattern in group):
198254
return True, idx
@@ -213,14 +269,50 @@ def _wait_for_any_stdout_growth_pattern_group(
213269
latest_segment = ""
214270

215271
while time.time() < deadline:
216-
latest_segment = self._read_stdout_from_offset(stdout_file, start_offset)
272+
latest_segment = self._read_monitor_output_from_offsets(stdout_file, start_offset)
217273
for idx, group in enumerate(alternative_pattern_groups):
218274
if all(pattern in latest_segment for pattern in group):
219275
return True, idx, latest_segment
220276
time.sleep(poll_interval)
221277

222278
return False, -1, latest_segment
223279

280+
def _wait_for_required_patterns_and_any_group(
281+
self,
282+
stdout_file: Path,
283+
*,
284+
start_offset: int,
285+
required_patterns: list[str],
286+
alternative_pattern_groups: list[list[str]],
287+
timeout_seconds: int = 120,
288+
poll_interval: float = 0.5,
289+
) -> tuple[bool, bool, int, str]:
290+
"""Wait until all fixed patterns and one alternative group are observed."""
291+
deadline = time.time() + timeout_seconds
292+
latest_segment = ""
293+
matched_group = -1
294+
295+
while time.time() < deadline:
296+
latest_segment = self._read_monitor_output_from_offsets(stdout_file, start_offset)
297+
fixed_ok = all(pattern in latest_segment for pattern in required_patterns)
298+
matched_group = -1
299+
for idx, group in enumerate(alternative_pattern_groups):
300+
if all(pattern in latest_segment for pattern in group):
301+
matched_group = idx
302+
break
303+
group_ok = matched_group >= 0
304+
if fixed_ok and group_ok:
305+
return True, True, matched_group, latest_segment
306+
time.sleep(poll_interval)
307+
308+
fixed_ok = all(pattern in latest_segment for pattern in required_patterns)
309+
matched_group = -1
310+
for idx, group in enumerate(alternative_pattern_groups):
311+
if all(pattern in latest_segment for pattern in group):
312+
matched_group = idx
313+
break
314+
return fixed_ok, matched_group >= 0, matched_group, latest_segment
315+
224316
def _wait_for_post_mutation_sync_complete(
225317
self,
226318
stdout_file: Path,
@@ -230,14 +322,20 @@ def _wait_for_post_mutation_sync_complete(
230322
poll_interval: float = 0.5,
231323
quiet_seconds_after_marker: float = 3.0,
232324
) -> tuple[bool, str]:
325+
"""Wait for a post-mutation global sync-complete marker when a test needs it.
326+
327+
Most local inotify tests should not use this helper. Local event handling
328+
can complete successfully without emitting another global sync-complete
329+
line, so those tests should wait for their event-specific patterns instead.
330+
"""
233331
deadline = time.time() + timeout_seconds
234332
latest_segment = ""
235333
marker_seen = False
236334
last_length = -1
237335
quiet_started_at: float | None = None
238336

239337
while time.time() < deadline:
240-
latest_segment = self._read_stdout_from_offset(stdout_file, start_offset)
338+
latest_segment = self._read_monitor_output_from_offsets(stdout_file, start_offset)
241339
now = time.time()
242340

243341
if self.SYNC_COMPLETE_PATTERN in latest_segment:

ci/e2e/testcases/tc0002_sync_list_validation.py

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import os
55
import re
66
import shutil
7+
import time
78
from dataclasses import dataclass, field
89
from pathlib import Path
910

@@ -454,6 +455,58 @@ def _run_standard_scenario(
454455
diffs = self._validate_scenario(scenario, fixture_events)
455456
return diffs, artifacts, metadata
456457

458+
def _is_retryable_cleanup_seed_failure(self, stdout: str, stderr: str) -> bool:
459+
content = f"{stdout}\n{stderr}"
460+
retryable_markers = (
461+
"HTTP request returned status code 408",
462+
"HTTP request returned status code 429",
463+
"HTTP request returned status code 500",
464+
"HTTP request returned status code 502",
465+
"HTTP request returned status code 503",
466+
"HTTP request returned status code 504",
467+
"Failed items to upload",
468+
"Parent path is not in the database or online",
469+
"Unable to upload this file",
470+
"std.json.JSONException",
471+
)
472+
return any(marker in content for marker in retryable_markers)
473+
474+
def _run_cleanup_seed_command_with_retry(
475+
self,
476+
context: E2EContext,
477+
command: list[str],
478+
*,
479+
max_attempts: int = 2,
480+
retry_sleep_seconds: float = 3.0,
481+
) -> tuple[object, int, list[str], str, str]:
482+
attempts = 0
483+
retry_reasons: list[str] = []
484+
stdout_segments: list[str] = []
485+
stderr_segments: list[str] = []
486+
last_result = None
487+
488+
for attempt in range(1, max_attempts + 1):
489+
attempts = attempt
490+
result = run_command(command, cwd=context.repo_root)
491+
last_result = result
492+
stdout_segments.append(f"\n===== cleanup seed attempt {attempt} stdout =====\n{result.stdout}")
493+
stderr_segments.append(f"\n===== cleanup seed attempt {attempt} stderr =====\n{result.stderr}")
494+
495+
if result.returncode == 0:
496+
break
497+
498+
if attempt >= max_attempts or not self._is_retryable_cleanup_seed_failure(result.stdout, result.stderr):
499+
break
500+
501+
retry_reasons.append(f"attempt {attempt} returned {result.returncode}; retryable seed failure marker observed")
502+
context.log(
503+
f"Scenario cleanup seed returned {result.returncode}; retrying attempt {attempt + 1}/{max_attempts}"
504+
)
505+
time.sleep(retry_sleep_seconds)
506+
507+
assert last_result is not None
508+
return last_result, attempts, retry_reasons, "".join(stdout_segments).lstrip("\n"), "".join(stderr_segments).lstrip("\n")
509+
457510
def _run_cleanup_regression_scenario(
458511
self,
459512
context: E2EContext,
@@ -500,13 +553,21 @@ def _run_cleanup_regression_scenario(
500553
sync_list_path.unlink()
501554

502555
phase1_command = self._build_sync_command(context, sync_root, config_dir)
503-
phase1_result = run_command(phase1_command, cwd=context.repo_root)
504-
write_text_file(phase1_stdout, phase1_result.stdout)
505-
write_text_file(phase1_stderr, phase1_result.stderr)
556+
(
557+
phase1_result,
558+
phase1_attempts,
559+
phase1_retry_reasons,
560+
phase1_stdout_text,
561+
phase1_stderr_text,
562+
) = self._run_cleanup_seed_command_with_retry(context, phase1_command)
563+
write_text_file(phase1_stdout, phase1_stdout_text)
564+
write_text_file(phase1_stderr, phase1_stderr_text)
506565
metadata.extend(
507566
[
508567
f"phase1_command={command_to_string(phase1_command)}",
509568
f"phase1_returncode={phase1_result.returncode}",
569+
f"phase1_attempts={phase1_attempts}",
570+
f"phase1_retry_reasons={phase1_retry_reasons!r}",
510571
]
511572
)
512573

ci/e2e/testcases/tc0020_monitor_mode_validation.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,14 +103,19 @@ def run(self, context: E2EContext) -> TestResult:
103103
)
104104

105105
mutation_log_start_offset = self._prepare_monitor_for_local_mutation(process, stdout_file, details)
106-
write_text_file(sync_root / root_name / "monitor-added.txt", "added while monitor mode was running\n")
107-
post_mutation_sync_complete, post_mutation_log_segment = self._wait_for_post_mutation_sync_complete(
106+
monitor_added_relative = f"{root_name}/monitor-added.txt"
107+
write_text_file(sync_root / monitor_added_relative, "added while monitor mode was running\n")
108+
required_patterns = [f"Uploading new file: {monitor_added_relative} ... done"]
109+
mutation_processed, post_mutation_log_segment = self._wait_for_stdout_growth_patterns(
108110
stdout_file,
109111
start_offset=mutation_log_start_offset,
112+
required_patterns=required_patterns,
110113
timeout_seconds=180,
111114
)
112-
details["post_mutation_sync_complete"] = post_mutation_sync_complete
115+
details["post_mutation_sync_complete"] = self.SYNC_COMPLETE_PATTERN in post_mutation_log_segment
116+
details["mutation_processed"] = mutation_processed
113117
details["post_mutation_log_segment_length"] = len(post_mutation_log_segment)
118+
details["mutation_required_patterns"] = required_patterns
114119
finally:
115120
self._shutdown_monitor_process(process, details)
116121

ci/e2e/testcases/tc0041_monitor_mode_local_create_upload.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,12 +194,13 @@ def run(self, context: E2EContext) -> TestResult:
194194
required_patterns = [
195195
f"Uploading new file: {created_relative} ... done",
196196
]
197-
post_mutation_sync_complete, post_mutation_log_segment = self._wait_for_post_mutation_sync_complete(
197+
mutation_processed, post_mutation_log_segment = self._wait_for_stdout_growth_patterns(
198198
monitor_stdout,
199199
start_offset=mutation_log_start_offset,
200+
required_patterns=required_patterns,
200201
timeout_seconds=180,
201202
)
202-
mutation_processed = all(pattern in post_mutation_log_segment for pattern in required_patterns)
203+
post_mutation_sync_complete = self.SYNC_COMPLETE_PATTERN in post_mutation_log_segment
203204
details["post_mutation_sync_complete"] = post_mutation_sync_complete
204205
details["mutation_processed"] = mutation_processed
205206
details["post_mutation_log_segment_length"] = len(post_mutation_log_segment)

ci/e2e/testcases/tc0042_monitor_mode_local_modify_upload.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -226,12 +226,13 @@ def run(self, context: E2EContext) -> TestResult:
226226
required_patterns = [
227227
f"Uploading modified file: {relative_path} ... done",
228228
]
229-
post_mutation_sync_complete, post_mutation_log_segment = self._wait_for_post_mutation_sync_complete(
229+
mutation_processed, post_mutation_log_segment = self._wait_for_stdout_growth_patterns(
230230
monitor_stdout,
231231
start_offset=mutation_log_start_offset,
232+
required_patterns=required_patterns,
232233
timeout_seconds=180,
233234
)
234-
mutation_processed = all(pattern in post_mutation_log_segment for pattern in required_patterns)
235+
post_mutation_sync_complete = self.SYNC_COMPLETE_PATTERN in post_mutation_log_segment
235236
details["post_mutation_sync_complete"] = post_mutation_sync_complete
236237
details["mutation_processed"] = mutation_processed
237238
details["post_mutation_log_segment_length"] = len(post_mutation_log_segment)

ci/e2e/testcases/tc0043_monitor_mode_local_delete_propagation.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,12 +229,13 @@ def run(self, context: E2EContext) -> TestResult:
229229
required_patterns = [
230230
f"Deleting item from Microsoft OneDrive: {delete_relative}",
231231
]
232-
post_mutation_sync_complete, post_mutation_log_segment = self._wait_for_post_mutation_sync_complete(
232+
mutation_processed, post_mutation_log_segment = self._wait_for_stdout_growth_patterns(
233233
monitor_stdout,
234234
start_offset=mutation_log_start_offset,
235+
required_patterns=required_patterns,
235236
timeout_seconds=180,
236237
)
237-
mutation_processed = all(pattern in post_mutation_log_segment for pattern in required_patterns)
238+
post_mutation_sync_complete = self.SYNC_COMPLETE_PATTERN in post_mutation_log_segment
238239
details["post_mutation_sync_complete"] = post_mutation_sync_complete
239240
details["mutation_processed"] = mutation_processed
240241
details["post_mutation_log_segment_length"] = len(post_mutation_log_segment)

ci/e2e/testcases/tc0044_monitor_mode_local_rename_propagation.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -231,18 +231,13 @@ def run(self, context: E2EContext) -> TestResult:
231231
f"Uploading new file: {new_relative} ... done",
232232
],
233233
]
234-
post_mutation_sync_complete, post_mutation_log_segment = self._wait_for_post_mutation_sync_complete(
234+
mutation_processed, matched_group, post_mutation_log_segment = self._wait_for_any_stdout_growth_pattern_group(
235235
monitor_stdout,
236236
start_offset=mutation_log_start_offset,
237+
alternative_pattern_groups=pattern_groups,
237238
timeout_seconds=180,
238239
)
239-
mutation_processed = False
240-
matched_group = -1
241-
for idx, group in enumerate(pattern_groups):
242-
if all(pattern in post_mutation_log_segment for pattern in group):
243-
mutation_processed = True
244-
matched_group = idx
245-
break
240+
post_mutation_sync_complete = self.SYNC_COMPLETE_PATTERN in post_mutation_log_segment
246241
details["post_mutation_sync_complete"] = post_mutation_sync_complete
247242
details["mutation_processed"] = mutation_processed
248243
details["matched_pattern_group_index"] = matched_group

ci/e2e/testcases/tc0045_monitor_mode_local_directory_create_propagation.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,12 +114,13 @@ def run(self, context: E2EContext) -> TestResult:
114114
required_patterns = [
115115
f"Uploading new file: {created_file_relative} ... done",
116116
]
117-
post_mutation_sync_complete, post_mutation_log_segment = self._wait_for_post_mutation_sync_complete(
117+
mutation_processed, post_mutation_log_segment = self._wait_for_stdout_growth_patterns(
118118
monitor_stdout,
119119
start_offset=mutation_log_start_offset,
120+
required_patterns=required_patterns,
120121
timeout_seconds=180,
121122
)
122-
mutation_processed = all(pattern in post_mutation_log_segment for pattern in required_patterns)
123+
post_mutation_sync_complete = self.SYNC_COMPLETE_PATTERN in post_mutation_log_segment
123124
details["post_mutation_sync_complete"] = post_mutation_sync_complete
124125
details["mutation_processed"] = mutation_processed
125126
details["post_mutation_log_segment_length"] = len(post_mutation_log_segment)

0 commit comments

Comments
 (0)