mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 12:45:17 +08:00
feat(sync): 接入解析缓存与汉化发布前置
补齐官方 release 解析缓存、TextUnit 明细索引、资源变更集、Crowdin handoff 预留、ResourceRepository 导入元数据和 localized release patch 前置链路。 同时开放文件级 patch.apply 与 UnityFS TextAsset/string/semantic field patch CLI/RPC 入口,并保留官方原版资源与汉化产物双目录发布状态。 验证:cargo test -p bat-assetbundle --locked;cargo clippy -p bat-assetbundle --all-targets --locked -- -D warnings;cargo test -p bat-infrastructure --locked。
This commit is contained in:
@@ -367,14 +367,16 @@ fn official_update_reuses_failed_staging_after_interrupted_download() {
|
||||
let curl_failure_state = harness.temp.path().join("curl-failure.state");
|
||||
write_executable(
|
||||
&harness.curl_script,
|
||||
&official_curl_script(
|
||||
&harness.curl_log,
|
||||
&zip_fixture,
|
||||
&resources_assets_path,
|
||||
fs::metadata(&resources_assets_path).unwrap().len() as usize,
|
||||
TEST_LAUNCHER_MANIFEST_SOURCE,
|
||||
Some(&curl_failure_state),
|
||||
),
|
||||
&official_curl_script(OfficialCurlScriptFixture {
|
||||
curl_log: &harness.curl_log,
|
||||
zip_fixture: &zip_fixture,
|
||||
resources_assets_fixture: &resources_assets_path,
|
||||
resources_assets_size: fs::metadata(&resources_assets_path).unwrap().len() as usize,
|
||||
manifest_source: TEST_LAUNCHER_MANIFEST_SOURCE,
|
||||
failure_state: Some(&curl_failure_state),
|
||||
clientpatch_unavailable: false,
|
||||
seed_unavailable_only: false,
|
||||
}),
|
||||
);
|
||||
|
||||
let config = harness.sync_config("failed-staging-output");
|
||||
@@ -399,6 +401,77 @@ fn official_update_reuses_failed_staging_after_interrupted_download() {
|
||||
assert!(version_state.current_completed_version.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn official_update_waits_when_client_patch_markers_are_not_ready() {
|
||||
let harness = TestHarness::new_with_clientpatch_unavailable(false);
|
||||
let config = harness.sync_config("clientpatch-marker-wait-output");
|
||||
let mut events = Vec::<OfficialUpdateProgress>::new();
|
||||
|
||||
let report = OfficialUpdateService::new()
|
||||
.run_with_progress(&config, |event| events.push(event))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
report.update_status,
|
||||
OfficialUpdateStatus::WaitingForOfficialResources
|
||||
);
|
||||
assert!(report.waiting_for_official_resources);
|
||||
assert!(!report.should_download);
|
||||
assert!(report
|
||||
.unavailable_endpoints
|
||||
.iter()
|
||||
.any(|endpoint| endpoint.url.ends_with("/TableBundles/TableCatalog.hash")));
|
||||
assert!(report
|
||||
.unavailable_endpoints
|
||||
.iter()
|
||||
.all(|endpoint| endpoint.error_kind == "http_forbidden"));
|
||||
assert!(report.staging_path.is_none());
|
||||
assert!(report.published_version_path.is_none());
|
||||
assert!(!config.output_root.join("current").exists());
|
||||
assert!(!config.output_root.join(".staging").exists());
|
||||
assert!(read_version_state(&report.version_state_path)
|
||||
.unwrap()
|
||||
.is_none());
|
||||
assert!(events
|
||||
.iter()
|
||||
.any(|event| event.stage == "upstream" && event.message.contains("尚未开放")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn official_update_waits_when_required_seed_catalog_is_not_ready() {
|
||||
let harness = TestHarness::new_with_clientpatch_unavailable(true);
|
||||
let config = harness.sync_config("clientpatch-seed-wait-output");
|
||||
let mut events = Vec::<OfficialUpdateProgress>::new();
|
||||
|
||||
let report = OfficialUpdateService::new()
|
||||
.run_with_progress(&config, |event| events.push(event))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
report.update_status,
|
||||
OfficialUpdateStatus::WaitingForOfficialResources
|
||||
);
|
||||
assert!(report.waiting_for_official_resources);
|
||||
assert_eq!(report.unavailable_endpoints.len(), 1);
|
||||
assert!(report.unavailable_endpoints[0]
|
||||
.url
|
||||
.ends_with("/TableBundles/TableCatalog.bytes"));
|
||||
assert_eq!(report.unavailable_endpoints[0].http_status, Some(403));
|
||||
assert!(report.staging_path.is_none());
|
||||
assert!(report.published_version_path.is_none());
|
||||
assert!(!config.output_root.join("current").exists());
|
||||
assert!(!config.output_root.join(".staging").exists());
|
||||
assert!(read_version_state(&report.version_state_path)
|
||||
.unwrap()
|
||||
.is_none());
|
||||
assert!(events
|
||||
.iter()
|
||||
.any(|event| event.stage == "catalog" && event.message.contains("TableCatalog.bytes")));
|
||||
assert!(events
|
||||
.iter()
|
||||
.any(|event| event.stage == "upstream" && event.message.contains("必需资源")));
|
||||
}
|
||||
|
||||
struct TestHarness {
|
||||
temp: TempDir,
|
||||
curl_script: std::path::PathBuf,
|
||||
@@ -412,6 +485,18 @@ impl TestHarness {
|
||||
}
|
||||
|
||||
fn new_with_manifest_source(manifest_source: &str) -> Self {
|
||||
Self::new_with_options(manifest_source, false, false)
|
||||
}
|
||||
|
||||
fn new_with_clientpatch_unavailable(seed_only: bool) -> Self {
|
||||
Self::new_with_options(TEST_LAUNCHER_MANIFEST_SOURCE, true, seed_only)
|
||||
}
|
||||
|
||||
fn new_with_options(
|
||||
manifest_source: &str,
|
||||
clientpatch_unavailable: bool,
|
||||
seed_unavailable_only: bool,
|
||||
) -> Self {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let resources_assets = synthetic_resources_assets();
|
||||
|
||||
@@ -425,14 +510,16 @@ impl TestHarness {
|
||||
let curl_script = temp.path().join("fake-curl");
|
||||
write_executable(
|
||||
&curl_script,
|
||||
&official_curl_script(
|
||||
&curl_log,
|
||||
&zip_fixture,
|
||||
&resources_assets_path,
|
||||
resources_assets.len(),
|
||||
&official_curl_script(OfficialCurlScriptFixture {
|
||||
curl_log: &curl_log,
|
||||
zip_fixture: &zip_fixture,
|
||||
resources_assets_fixture: &resources_assets_path,
|
||||
resources_assets_size: resources_assets.len(),
|
||||
manifest_source,
|
||||
None,
|
||||
),
|
||||
failure_state: None,
|
||||
clientpatch_unavailable,
|
||||
seed_unavailable_only,
|
||||
}),
|
||||
);
|
||||
|
||||
let unzip_script = temp.path().join("fake-unzip");
|
||||
@@ -542,15 +629,20 @@ fn one_file_zip(name: &[u8], data: &[u8]) -> Vec<u8> {
|
||||
bytes
|
||||
}
|
||||
|
||||
fn official_curl_script(
|
||||
curl_log: &Path,
|
||||
zip_fixture: &Path,
|
||||
resources_assets_fixture: &Path,
|
||||
struct OfficialCurlScriptFixture<'a> {
|
||||
curl_log: &'a Path,
|
||||
zip_fixture: &'a Path,
|
||||
resources_assets_fixture: &'a Path,
|
||||
resources_assets_size: usize,
|
||||
manifest_source: &str,
|
||||
failure_state: Option<&Path>,
|
||||
) -> String {
|
||||
let failure_state = failure_state
|
||||
manifest_source: &'a str,
|
||||
failure_state: Option<&'a Path>,
|
||||
clientpatch_unavailable: bool,
|
||||
seed_unavailable_only: bool,
|
||||
}
|
||||
|
||||
fn official_curl_script(fixture: OfficialCurlScriptFixture<'_>) -> String {
|
||||
let failure_state = fixture
|
||||
.failure_state
|
||||
.map(shell_quote)
|
||||
.unwrap_or_else(|| "''".to_string());
|
||||
format!(
|
||||
@@ -614,6 +706,31 @@ maybe_fail_once() {{
|
||||
fi
|
||||
}}
|
||||
|
||||
clientpatch_is_unavailable() {{
|
||||
if [[ "{clientpatch_unavailable}" != "true" ]]; then
|
||||
return 1
|
||||
fi
|
||||
if [[ "$url" != "{addressables_root}/"* ]]; then
|
||||
return 1
|
||||
fi
|
||||
if [[ "{seed_unavailable_only}" == "true" ]]; then
|
||||
case "$url" in
|
||||
*/TableBundles/TableCatalog.bytes|*/BundlePackingInfo.bytes|*/Catalog/MediaCatalog.bytes)
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
return 0
|
||||
}}
|
||||
|
||||
if clientpatch_is_unavailable; then
|
||||
echo "curl: (22) The requested URL returned error: 403" >&2
|
||||
exit 22
|
||||
fi
|
||||
|
||||
if [[ "$url" == "https://api-launcher-jp.yo-star.com/api/launcher/game/config" ]]; then
|
||||
cat <<'JSON'
|
||||
{{"code":200,"message":"ok","data":{{"game_latest_version":"{launcher_latest_version}","game_latest_file_path":"{launcher_latest_file_path}"}}}}
|
||||
@@ -679,16 +796,16 @@ else
|
||||
exit 1
|
||||
fi
|
||||
"#,
|
||||
shell_quote(curl_log),
|
||||
shell_quote(zip_fixture),
|
||||
shell_quote(resources_assets_fixture),
|
||||
shell_quote(fixture.curl_log),
|
||||
shell_quote(fixture.zip_fixture),
|
||||
shell_quote(fixture.resources_assets_fixture),
|
||||
launcher_latest_version = TEST_LAUNCHER_LATEST_VERSION,
|
||||
launcher_latest_file_path = TEST_LAUNCHER_LATEST_FILE_PATH,
|
||||
launcher_game_config_json_url = TEST_LAUNCHER_GAME_CONFIG_JSON_URL,
|
||||
launcher_manifest_url = TEST_LAUNCHER_MANIFEST_URL,
|
||||
launcher_manifest_source = manifest_source,
|
||||
launcher_manifest_source = fixture.manifest_source,
|
||||
launcher_resources_assets_url = TEST_LAUNCHER_RESOURCES_ASSETS_URL,
|
||||
resources_assets_size = resources_assets_size,
|
||||
resources_assets_size = fixture.resources_assets_size,
|
||||
server_info_url = TEST_SERVER_INFO_URL,
|
||||
connection_group = TEST_CONNECTION_GROUP,
|
||||
addressables_root = TEST_ADDRESSABLES_ROOT,
|
||||
@@ -698,6 +815,8 @@ fi
|
||||
android_bundle_catalog_hash = xxhash32(b"FullPatch_001.zip"),
|
||||
android_media_catalog_hash = xxhash32(b"GameData\\Audio\\VOC_JP\\JP_Airi_Android.zip"),
|
||||
failure_state = failure_state,
|
||||
clientpatch_unavailable = fixture.clientpatch_unavailable,
|
||||
seed_unavailable_only = fixture.seed_unavailable_only,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user