mirror of
https://github.com/abraunegg/onedrive.git
synced 2026-10-23 03:08:55 +03:00
461 lines
25 KiB
Python
461 lines
25 KiB
Python
#!/usr/bin/env python3
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import shutil
|
|
import sys
|
|
import traceback
|
|
from pathlib import Path
|
|
|
|
from framework.auth_preflight import run_auth_preflight
|
|
from framework.context import E2EContext
|
|
from framework.database_capture import capture_onedrive_databases
|
|
from framework.result import TestResult
|
|
from framework.utils import ensure_directory, perform_full_account_cleanup, write_text_file
|
|
from testcases.tc0001_basic_resync import TestCase0001BasicResync
|
|
from testcases.tc0002_sync_list_validation import TestCase0002SyncListValidation
|
|
from testcases.tc0003_dry_run_validation import TestCase0003DryRunValidation
|
|
from testcases.tc0004_single_directory_sync import TestCase0004SingleDirectorySync
|
|
from testcases.tc0005_force_sync_override import TestCase0005ForceSyncOverride
|
|
from testcases.tc0006_download_only import TestCase0006DownloadOnly
|
|
from testcases.tc0007_download_only_cleanup_local_files import TestCase0007DownloadOnlyCleanupLocalFiles
|
|
from testcases.tc0008_upload_only import TestCase0008UploadOnly
|
|
from testcases.tc0009_upload_only_no_remote_delete import TestCase0009UploadOnlyNoRemoteDelete
|
|
from testcases.tc0010_upload_only_remove_source_files import TestCase0010UploadOnlyRemoveSourceFiles
|
|
from testcases.tc0011_skip_file_validation import TestCase0011SkipFileValidation
|
|
from testcases.tc0012_skip_dir_validation import TestCase0012SkipDirValidation
|
|
from testcases.tc0013_skip_dotfiles_validation import TestCase0013SkipDotfilesValidation
|
|
from testcases.tc0014_skip_size_validation import TestCase0014SkipSizeValidation
|
|
from testcases.tc0015_skip_symlinks_validation import TestCase0015SkipSymlinksValidation
|
|
from testcases.tc0016_check_nosync_validation import TestCase0016CheckNosyncValidation
|
|
from testcases.tc0017_check_nomount_validation import TestCase0017CheckNomountValidation
|
|
from testcases.tc0018_recycle_bin_validation import TestCase0018RecycleBinValidation
|
|
from testcases.tc0019_logging_and_running_config import TestCase0019LoggingAndRunningConfig
|
|
from testcases.tc0020_monitor_mode_validation import TestCase0020MonitorModeValidation
|
|
from testcases.tc0021_resumable_transfers_validation import TestCase0021ResumableTransfersValidation
|
|
from testcases.tc0022_local_first_validation import TestCase0022LocalFirstValidation
|
|
from testcases.tc0023_bypass_data_preservation_validation import TestCase0023BypassDataPreservationValidation
|
|
from testcases.tc0024_big_delete_safeguard_validation import TestCase0024BigDeleteSafeguardValidation
|
|
from testcases.tc0025_invalid_character_filename_validation import TestCase0025InvalidCharacterFilenameValidation
|
|
from testcases.tc0026_reserved_device_name_validation import TestCase0026ReservedDeviceNameValidation
|
|
from testcases.tc0027_whitespace_trailing_dot_validation import TestCase0027WhitespaceTrailingDotValidation
|
|
from testcases.tc0028_control_character_non_utf8_filename_validation import TestCase0028ControlCharacterNonUtf8FilenameValidation
|
|
from testcases.tc0029_local_first_upload_only_timestamp_preservation_validation import TestCase0029LocalFirstUploadOnlyTimestampPreservationValidation
|
|
from testcases.tc0030_local_rename_propagation_validation import TestCase0030LocalRenamePropagationValidation
|
|
from testcases.tc0031_local_directory_rename_propagation_validation import TestCase0031LocalDirectoryRenamePropagationValidation
|
|
from testcases.tc0032_remote_rename_reconciliation import TestCase0032RemoteRenameReconciliation
|
|
from testcases.tc0033_remote_directory_rename_reconciliation import TestCase0033RemoteDirectoryRenameReconciliation
|
|
from testcases.tc0034_local_move_between_directories_validation import TestCase0034LocalMoveBetweenDirectoriesValidation
|
|
from testcases.tc0035_remote_move_between_directories_reconciliation import TestCase0035RemoteMoveBetweenDirectoriesReconciliation
|
|
from testcases.tc0036_overwrite_replace_existing_file_content_validation import TestCase0036OverwriteReplaceExistingFileContentValidation
|
|
from testcases.tc0037_mtime_only_local_change_handling import TestCase0037MtimeOnlyLocalChangeHandling
|
|
from testcases.tc0038_delete_and_recreate_with_same_name_validation import TestCase0038DeleteAndRecreateWithSameNameValidation
|
|
from testcases.tc0039_empty_directory_handling_validation import TestCase0039EmptyDirectoryHandling
|
|
from testcases.tc0040_nested_tree_rename_and_delete_stress_validation import TestCase0040NestedTreeRenameAndDeleteStressValidation
|
|
from testcases.tc0041_monitor_mode_local_create_upload import TestCase0041MonitorModeLocalCreateUpload
|
|
from testcases.tc0042_monitor_mode_local_modify_upload import TestCase0042MonitorModeLocalModifyUpload
|
|
from testcases.tc0043_monitor_mode_local_delete_propagation import TestCase0043MonitorModeLocalDeletePropagation
|
|
from testcases.tc0044_monitor_mode_local_rename_propagation import TestCase0044MonitorModeLocalRenamePropagation
|
|
from testcases.tc0045_monitor_mode_local_directory_create_propagation import TestCase0045MonitorModeLocalDirectoryCreatePropagation
|
|
from testcases.tc0046_monitor_mode_local_directory_rename_propagation import TestCase0046MonitorModeLocalDirectoryRenamePropagation
|
|
from testcases.tc0047_monitor_mode_local_directory_delete_propagation import TestCase0047MonitorModeLocalDirectoryDeletePropagation
|
|
from testcases.tc0048_monitor_mode_burst_local_file_creates import TestCase0048MonitorModeBurstLocalFileCreates
|
|
from testcases.tc0049_monitor_mode_mixed_burst_operations import TestCase0049MonitorModeMixedBurstOperations
|
|
from testcases.tc0050_monitor_mode_nested_file_create_inside_new_directory import TestCase0050MonitorModeNestedFileCreateInsideNewDirectory
|
|
from testcases.tc0051_monitor_mode_mtime_only_local_change_handling import TestCase0051MonitorModeMtimeOnlyLocalChangeHandling
|
|
from testcases.tc0052_monitor_mode_large_file_create_session_upload import TestCase0052MonitorModeLargeFileCreateSessionUpload
|
|
from testcases.tc0053_monitor_mode_rename_then_modify import TestCase0053MonitorModeRenameThenModify
|
|
from testcases.tc0054_monitor_mode_atomic_save_editor_replace_workflow import TestCase0054MonitorModeAtomicSaveEditorReplaceWorkflow
|
|
from testcases.tc0055_upload_only_remove_source_folders import TestCase0055UploadOnlyRemoveSourceFolders
|
|
from testcases.tc0056_monitor_mode_create_then_delete_quickly import TestCase0056MonitorModeCreateThenDeleteQuickly
|
|
from testcases.tc0057_recycle_bin_delete_behaviour_validation import TestCase0057RecycleBinDeleteBehaviourValidation
|
|
from testcases.tc0058_monitor_download_only_cleanup_cadence import TestCase0058MonitorDownloadOnlyCleanupCadence
|
|
from testcases.tc0059_websocket_remote_upload_notification import TestCase0059WebSocketRemoteUploadNotification
|
|
from testcases.tc0060_monitor_mode_local_move_no_delete_reupload import TestCase0060MonitorModeLocalMoveNoDeleteReupload
|
|
from testcases.tc0061_remote_move_into_skip_dir_reconciliation import TestCase0061RemoteMoveIntoSkipDirReconciliation
|
|
from testcases.tc0062_transfer_metrics_validation import TestCase0062TransferMetricsValidation
|
|
from testcases.tc0063_local_parent_rename_during_download import TestCase0063LocalParentRenameDuringDownload
|
|
from testcases.tc0064_mirror_local_state_remote_cleanup import TestCase0064MirrorLocalStateRemoteCleanup
|
|
from testcases.tc0065_sync_list_remote_directory_move_reconciliation import TestCase0065SyncListRemoteDirectoryMoveReconciliation
|
|
from testcases.tc0066_safebackup_transactional_replacement_validation import TestCase0066SafeBackupTransactionalReplacementValidation
|
|
from testcases.tc0067_safebackup_interrupted_replacement_validation import TestCase0067SafeBackupInterruptedReplacementValidation
|
|
from testcases.tc0068_safebackup_upload_only_conflict_validation import TestCase0068SafeBackupUploadOnlyConflictValidation
|
|
from testcases.tc0069_safebackup_remote_move_destination_collision_validation import TestCase0069SafeBackupRemoteMoveDestinationCollisionValidation
|
|
from testcases.tc0070_safebackup_new_file_upload_collision_validation import TestCase0070SafeBackupNewFileUploadCollisionValidation
|
|
from testcases.tc0071_safebackup_metadata_only_identity_validation import TestCase0071SafeBackupMetadataOnlyIdentityValidation
|
|
from testcases.tc0072_safebackup_preservation_failure_aborts_replacement import TestCase0072SafeBackupPreservationFailureAbortsReplacement
|
|
from testcases.tc0073_safebackup_existing_preservation_reuse_validation import TestCase0073SafeBackupExistingPreservationReuseValidation
|
|
from testcases.tc0074_safebackup_clean_resync_no_conflict_validation import TestCase0074SafeBackupCleanResyncNoConflictValidation
|
|
from testcases.tc0075_safebackup_remote_delete_local_modify_validation import TestCase0075SafeBackupRemoteDeleteLocalModifyValidation
|
|
from testcases.tc0076_safebackup_resync_content_conflict_validation import TestCase0076SafeBackupResyncContentConflictValidation
|
|
from testcases.tc0077_timestamp_authority_validation import TestCase0077TimestampAuthorityValidation
|
|
from testcases.tc0078_monitor_mode_rename_to_invalid_name import TestCase0078MonitorModeRenameToInvalidName
|
|
from testcases.tc0079_display_sync_status_validation import TestCase0079DisplaySyncStatusValidation
|
|
from testcases.tc0080_monitor_remote_directory_rename_reconciliation import TestCase0080MonitorRemoteDirectoryRenameReconciliation
|
|
from testcases.tc0081_historical_database_upgrade_validation import TestCase0081HistoricalDatabaseUpgradeValidation
|
|
from testcases.tc0082_single_directory_delete_reconciliation import TestCase0082SingleDirectoryDeleteReconciliation
|
|
from testcases.tc0083_delta_children_reconciliation_parity import TestCase0083DeltaChildrenReconciliationParity
|
|
from testcases.tc0084_remote_batch_deletion_reconciliation import TestCase0084RemoteBatchDeletionReconciliation
|
|
from testcases.tc0085_websocket_wake_during_active_upload import TestCase0085WebSocketWakeDuringActiveUpload
|
|
from testcases.tc0086_https_credential_redaction import TestCase0086HTTPSCredentialRedaction
|
|
from testcases.tc0087_resync_monitor_working_directory_boundary import TestCase0087ResyncMonitorWorkingDirectoryBoundary
|
|
|
|
|
|
def build_test_suite() -> list:
|
|
return [
|
|
TestCase0001BasicResync(),
|
|
TestCase0002SyncListValidation(),
|
|
TestCase0003DryRunValidation(),
|
|
TestCase0004SingleDirectorySync(),
|
|
TestCase0005ForceSyncOverride(),
|
|
TestCase0006DownloadOnly(),
|
|
TestCase0007DownloadOnlyCleanupLocalFiles(),
|
|
TestCase0008UploadOnly(),
|
|
TestCase0009UploadOnlyNoRemoteDelete(),
|
|
TestCase0010UploadOnlyRemoveSourceFiles(),
|
|
TestCase0011SkipFileValidation(),
|
|
TestCase0012SkipDirValidation(),
|
|
TestCase0013SkipDotfilesValidation(),
|
|
TestCase0014SkipSizeValidation(),
|
|
TestCase0015SkipSymlinksValidation(),
|
|
TestCase0016CheckNosyncValidation(),
|
|
TestCase0017CheckNomountValidation(),
|
|
TestCase0018RecycleBinValidation(),
|
|
TestCase0019LoggingAndRunningConfig(),
|
|
TestCase0020MonitorModeValidation(),
|
|
TestCase0021ResumableTransfersValidation(),
|
|
TestCase0022LocalFirstValidation(),
|
|
TestCase0023BypassDataPreservationValidation(),
|
|
TestCase0024BigDeleteSafeguardValidation(),
|
|
TestCase0025InvalidCharacterFilenameValidation(),
|
|
TestCase0026ReservedDeviceNameValidation(),
|
|
TestCase0027WhitespaceTrailingDotValidation(),
|
|
TestCase0028ControlCharacterNonUtf8FilenameValidation(),
|
|
TestCase0029LocalFirstUploadOnlyTimestampPreservationValidation(),
|
|
TestCase0030LocalRenamePropagationValidation(),
|
|
TestCase0031LocalDirectoryRenamePropagationValidation(),
|
|
TestCase0032RemoteRenameReconciliation(),
|
|
TestCase0033RemoteDirectoryRenameReconciliation(),
|
|
TestCase0034LocalMoveBetweenDirectoriesValidation(),
|
|
TestCase0035RemoteMoveBetweenDirectoriesReconciliation(),
|
|
TestCase0036OverwriteReplaceExistingFileContentValidation(),
|
|
TestCase0037MtimeOnlyLocalChangeHandling(),
|
|
TestCase0038DeleteAndRecreateWithSameNameValidation(),
|
|
TestCase0039EmptyDirectoryHandling(),
|
|
TestCase0040NestedTreeRenameAndDeleteStressValidation(),
|
|
TestCase0041MonitorModeLocalCreateUpload(),
|
|
TestCase0042MonitorModeLocalModifyUpload(),
|
|
TestCase0043MonitorModeLocalDeletePropagation(),
|
|
TestCase0044MonitorModeLocalRenamePropagation(),
|
|
TestCase0045MonitorModeLocalDirectoryCreatePropagation(),
|
|
TestCase0046MonitorModeLocalDirectoryRenamePropagation(),
|
|
TestCase0047MonitorModeLocalDirectoryDeletePropagation(),
|
|
TestCase0048MonitorModeBurstLocalFileCreates(),
|
|
TestCase0049MonitorModeMixedBurstOperations(),
|
|
TestCase0050MonitorModeNestedFileCreateInsideNewDirectory(),
|
|
TestCase0051MonitorModeMtimeOnlyLocalChangeHandling(),
|
|
TestCase0052MonitorModeLargeFileCreateSessionUpload(),
|
|
TestCase0053MonitorModeRenameThenModify(),
|
|
TestCase0054MonitorModeAtomicSaveEditorReplaceWorkflow(),
|
|
TestCase0055UploadOnlyRemoveSourceFolders(),
|
|
TestCase0056MonitorModeCreateThenDeleteQuickly(),
|
|
TestCase0057RecycleBinDeleteBehaviourValidation(),
|
|
TestCase0058MonitorDownloadOnlyCleanupCadence(),
|
|
TestCase0059WebSocketRemoteUploadNotification(),
|
|
TestCase0060MonitorModeLocalMoveNoDeleteReupload(),
|
|
TestCase0061RemoteMoveIntoSkipDirReconciliation(),
|
|
TestCase0062TransferMetricsValidation(),
|
|
TestCase0063LocalParentRenameDuringDownload(),
|
|
TestCase0064MirrorLocalStateRemoteCleanup(),
|
|
TestCase0065SyncListRemoteDirectoryMoveReconciliation(),
|
|
TestCase0066SafeBackupTransactionalReplacementValidation(),
|
|
TestCase0067SafeBackupInterruptedReplacementValidation(),
|
|
TestCase0068SafeBackupUploadOnlyConflictValidation(),
|
|
TestCase0069SafeBackupRemoteMoveDestinationCollisionValidation(),
|
|
TestCase0070SafeBackupNewFileUploadCollisionValidation(),
|
|
TestCase0071SafeBackupMetadataOnlyIdentityValidation(),
|
|
TestCase0072SafeBackupPreservationFailureAbortsReplacement(),
|
|
TestCase0073SafeBackupExistingPreservationReuseValidation(),
|
|
TestCase0074SafeBackupCleanResyncNoConflictValidation(),
|
|
TestCase0075SafeBackupRemoteDeleteLocalModifyValidation(),
|
|
TestCase0076SafeBackupResyncContentConflictValidation(),
|
|
TestCase0077TimestampAuthorityValidation(),
|
|
TestCase0078MonitorModeRenameToInvalidName(),
|
|
TestCase0079DisplaySyncStatusValidation(),
|
|
TestCase0080MonitorRemoteDirectoryRenameReconciliation(),
|
|
TestCase0081HistoricalDatabaseUpgradeValidation(),
|
|
TestCase0082SingleDirectoryDeleteReconciliation(),
|
|
TestCase0083DeltaChildrenReconciliationParity(),
|
|
TestCase0084RemoteBatchDeletionReconciliation(),
|
|
TestCase0085WebSocketWakeDuringActiveUpload(),
|
|
TestCase0086HTTPSCredentialRedaction(),
|
|
TestCase0087ResyncMonitorWorkingDirectoryBoundary(),
|
|
]
|
|
|
|
|
|
def _normalise_case_id(value: str) -> str:
|
|
text = value.strip()
|
|
if not text:
|
|
return ""
|
|
if text.lower().startswith("tc"):
|
|
text = text[2:]
|
|
if text.isdigit() and len(text) <= 4:
|
|
text = text.zfill(4)
|
|
return text
|
|
|
|
|
|
def _apply_cli_overrides(args: argparse.Namespace) -> None:
|
|
if args.debug:
|
|
os.environ["E2E_DEBUG"] = "1"
|
|
if args.skip_suite_cleanup:
|
|
os.environ["E2E_SKIP_SUITE_CLEANUP"] = "1"
|
|
if args.output_subdir:
|
|
os.environ["E2E_OUTPUT_SUBDIR"] = args.output_subdir
|
|
if args.run_label:
|
|
os.environ["E2E_RUN_LABEL"] = args.run_label
|
|
|
|
if args.case_id:
|
|
selected: list[str] = []
|
|
for raw_value in args.case_id:
|
|
for token in raw_value.split(","):
|
|
case_id = _normalise_case_id(token)
|
|
if case_id:
|
|
selected.append(case_id)
|
|
if selected:
|
|
os.environ["E2E_SELECTED_CASES"] = ",".join(sorted(set(selected)))
|
|
|
|
if args.scenario:
|
|
mapping: dict[str, set[str]] = {}
|
|
for raw_value in args.scenario:
|
|
case_part, separator, scenario_part = raw_value.partition(":")
|
|
if separator != ":":
|
|
raise RuntimeError(f"Invalid --scenario value '{raw_value}'. Expected CASE:SCENARIO[,SCENARIO]")
|
|
case_id = _normalise_case_id(case_part)
|
|
if not case_id:
|
|
raise RuntimeError(f"Invalid case id in --scenario value '{raw_value}'")
|
|
mapping.setdefault(case_id, set())
|
|
for token in scenario_part.split(","):
|
|
scenario_id = token.strip()
|
|
if scenario_id:
|
|
mapping[case_id].add(scenario_id)
|
|
if mapping:
|
|
os.environ["E2E_SELECTED_SCENARIOS_JSON"] = json.dumps({key: sorted(value) for key, value in mapping.items()}, sort_keys=True)
|
|
|
|
|
|
|
|
|
|
def result_to_actions_case(result: TestResult) -> dict:
|
|
output = {
|
|
"id": result.case_id,
|
|
"name": result.name,
|
|
"status": result.status,
|
|
}
|
|
|
|
if result.reason:
|
|
output["reason"] = result.reason
|
|
|
|
if result.artifacts:
|
|
output["artifacts"] = result.artifacts
|
|
|
|
if result.details:
|
|
output["details"] = result.details
|
|
|
|
return output
|
|
|
|
|
|
def _build_metadata(context: E2EContext, selected_case_ids: list[str], executed_case_ids: list[str]) -> dict:
|
|
return {
|
|
"target": context.e2e_target,
|
|
"run_id": context.run_id,
|
|
"run_label": context.run_label,
|
|
"account_label": context.account_label,
|
|
"debug_enabled": context.debug_enabled,
|
|
"skip_suite_cleanup": context.skip_suite_cleanup,
|
|
"selected_case_ids": selected_case_ids,
|
|
"selected_scenarios": {case_id: sorted(values) for case_id, values in sorted(context.selected_scenarios.items())},
|
|
"executed_case_ids": executed_case_ids,
|
|
}
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="Run OneDrive E2E test cases")
|
|
parser.add_argument("--case-id", action="append", help="Run only the specified case id(s). Supports comma-separated values.")
|
|
parser.add_argument("--scenario", action="append", help="Run only the specified scenario(s) for a case. Format: CASE:SCENARIO[,SCENARIO]")
|
|
parser.add_argument("--debug", action="store_true", help="Rerun with debug verbosity enabled")
|
|
parser.add_argument("--skip-suite-cleanup", action="store_true", help="Skip initial full-account cleanup")
|
|
parser.add_argument("--output-subdir", help="Write results into ci/e2e/out/<subdir>")
|
|
parser.add_argument("--run-label", help="Logical label for this run (for metadata/logging)")
|
|
args = parser.parse_args()
|
|
|
|
_apply_cli_overrides(args)
|
|
context = E2EContext.from_environment()
|
|
context.prepare_runtime()
|
|
|
|
selected_case_ids = sorted(context.selected_case_ids)
|
|
cases_to_run = [testcase for testcase in build_test_suite() if context.should_run_case(testcase.case_id)]
|
|
executed_case_ids = [testcase.case_id for testcase in cases_to_run]
|
|
|
|
suite_metadata = _build_metadata(context, selected_case_ids, executed_case_ids)
|
|
|
|
auth_preflight_result = run_auth_preflight(context)
|
|
if auth_preflight_result.status != "pass":
|
|
context.log(f"Auth preflight FAILED: {auth_preflight_result.reason}")
|
|
results = {
|
|
**suite_metadata,
|
|
"cases": [result_to_actions_case(auth_preflight_result)],
|
|
}
|
|
results_file = context.out_dir / "results.json"
|
|
write_text_file(results_file, json.dumps(results, indent=2, sort_keys=False))
|
|
return 1
|
|
|
|
context.log("Auth preflight completed successfully")
|
|
|
|
if not context.skip_suite_cleanup:
|
|
context.bootstrap_suite_cleanup_config_dir()
|
|
context.log("Starting suite-wide cleanup of local and remote OneDrive content")
|
|
|
|
cleanup_ok, cleanup_reason, cleanup_artifacts, cleanup_details = perform_full_account_cleanup(
|
|
onedrive_bin=context.onedrive_bin,
|
|
repo_root=context.repo_root,
|
|
config_dir=context.suite_cleanup_config_dir,
|
|
sync_dir=context.default_sync_dir,
|
|
log_dir=context.suite_cleanup_log_dir,
|
|
)
|
|
if not cleanup_ok:
|
|
context.log(f"Suite cleanup FAILED: {cleanup_reason}")
|
|
|
|
results = {
|
|
**suite_metadata,
|
|
"cases": [
|
|
{
|
|
"id": "0000",
|
|
"name": "suite cleanup",
|
|
"status": "fail",
|
|
"reason": cleanup_reason,
|
|
"artifacts": cleanup_artifacts,
|
|
"details": cleanup_details,
|
|
}
|
|
],
|
|
}
|
|
|
|
results_file = context.out_dir / "results.json"
|
|
results_json = json.dumps(results, indent=2, sort_keys=False)
|
|
write_text_file(results_file, results_json)
|
|
return 1
|
|
|
|
context.log("Suite-wide cleanup completed successfully")
|
|
else:
|
|
context.log("Skipping suite-wide cleanup because E2E_SKIP_SUITE_CLEANUP is enabled")
|
|
|
|
context.log(
|
|
f"Initialising E2E framework for target='{context.e2e_target}', "
|
|
f"run_id='{context.run_id}', run_label='{context.run_label}', debug_enabled={context.debug_enabled}"
|
|
)
|
|
|
|
cases = []
|
|
failed = False
|
|
|
|
try:
|
|
for testcase in cases_to_run:
|
|
context.log(f"Starting test case {testcase.case_id}: {testcase.name}")
|
|
|
|
try:
|
|
result = testcase.run(context)
|
|
|
|
if result.case_id != testcase.case_id:
|
|
raise RuntimeError(
|
|
f"Test case returned mismatched case_id: expected '{testcase.case_id}', got '{result.case_id}'"
|
|
)
|
|
|
|
cases.append(result_to_actions_case(result))
|
|
|
|
if result.status != "pass":
|
|
failed = True
|
|
captured_databases = capture_onedrive_databases(context, testcase.case_id, reason="failure")
|
|
if captured_databases:
|
|
result.artifacts.extend(captured_databases)
|
|
result.details.setdefault("captured_database_files", captured_databases)
|
|
cases[-1] = result_to_actions_case(result)
|
|
context.log(f"Test case {testcase.case_id} FAILED: {result.reason or 'no reason provided'}")
|
|
else:
|
|
context.log(f"Test case {testcase.case_id} PASSED")
|
|
|
|
except Exception as exc:
|
|
failed = True
|
|
tb = traceback.format_exc()
|
|
|
|
context.log(f"Unhandled exception in test case {testcase.case_id}: {exc}")
|
|
context.log(tb)
|
|
|
|
error_log = context.logs_dir / f"{testcase.case_id}_exception.log"
|
|
write_text_file(error_log, tb)
|
|
|
|
failure_result = TestResult(
|
|
case_id=testcase.case_id,
|
|
name=testcase.name,
|
|
status="fail",
|
|
reason=f"Unhandled exception: {exc}",
|
|
artifacts=[str(error_log)],
|
|
details={"exception_type": type(exc).__name__},
|
|
)
|
|
captured_databases = capture_onedrive_databases(context, testcase.case_id, reason="failure")
|
|
if captured_databases:
|
|
failure_result.artifacts.extend(captured_databases)
|
|
failure_result.details["captured_database_files"] = captured_databases
|
|
cases.append(result_to_actions_case(failure_result))
|
|
|
|
finally:
|
|
# Never destroy evidence during a debug rerun. Primary suites should
|
|
# clean after tests even if one of them throws an exception.
|
|
if not context.skip_suite_cleanup:
|
|
context.log("Starting end-of-suite permanent cleanup")
|
|
try:
|
|
cleanup_ok, cleanup_reason, cleanup_artifacts, cleanup_details = perform_full_account_cleanup(
|
|
onedrive_bin=context.onedrive_bin,
|
|
repo_root=context.repo_root,
|
|
config_dir=context.suite_cleanup_config_dir,
|
|
sync_dir=context.default_sync_dir,
|
|
log_dir=context.logs_dir / "_suite_cleanup_final",
|
|
)
|
|
if not cleanup_ok:
|
|
failed = True
|
|
cases.append({
|
|
"id": "9999", "name": "final suite cleanup", "status": "fail",
|
|
"reason": cleanup_reason, "artifacts": cleanup_artifacts,
|
|
"details": cleanup_details,
|
|
})
|
|
context.log(f"Final suite cleanup FAILED: {cleanup_reason}")
|
|
else:
|
|
context.log("Final suite cleanup completed and active remote state verified")
|
|
except Exception as exc:
|
|
failed = True
|
|
error_log = context.logs_dir / "final_cleanup_exception.log"
|
|
write_text_file(error_log, traceback.format_exc())
|
|
cases.append({
|
|
"id": "9999", "name": "final suite cleanup", "status": "fail",
|
|
"reason": f"Exception during final cleanup: {exc}",
|
|
"artifacts": [str(error_log)],
|
|
})
|
|
context.log(f"Final suite cleanup threw an exception: {exc}")
|
|
finally:
|
|
shutil.rmtree(context.suite_cleanup_config_dir, ignore_errors=True)
|
|
|
|
results = {
|
|
**suite_metadata,
|
|
"cases": cases,
|
|
}
|
|
|
|
results_file = context.out_dir / "results.json"
|
|
results_json = json.dumps(results, indent=2, sort_keys=False)
|
|
write_text_file(results_file, results_json)
|
|
|
|
context.log(f"Wrote results to {results_file}")
|
|
|
|
return 1 if failed else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|