backbeat_sdk 0.5.0-beta

The library for interfacing with Backbeat.
#![allow(unreachable_pub, clippy::all, clippy::restriction)]
#![allow(clippy::pedantic, clippy::nursery, clippy::cargo)]

use std::cell::RefCell;
use std::rc::Rc;

use backbeat_core::{
	AssetId, AssetPath, Assets, BackbeatFile, ChartData, ChartDesc, ChartFilename, Sha256,
};
use backbeat_store_config::{BackbeatConfig, ServerConfig};
use serde_json::json;

mod support;

struct Fixture {
	_server_runtime: tokio::runtime::Runtime,
	url: String,
	bundle: BackbeatFile,
	asset_id: AssetId,
}

impl Fixture {
	fn new() -> Self {
		let server_runtime = tokio::runtime::Builder::new_multi_thread()
			.worker_threads(1)
			.enable_all()
			.build()
			.unwrap();
		let data = b"runtime independent asset".to_vec();
		let asset_id = AssetId(Sha256::checksum_bytes(&data));
		let bundle = BackbeatFile {
			filename: ChartFilename::new("runtime.bms").unwrap(),
			assets: Assets::from([(AssetPath::new("song.wav").unwrap(), asset_id)]),
			desc: ChartDesc::new("test").unwrap(),
			chart: ChartData::compress(b"#PLAYER 1\n#TITLE Runtime\n#BPM 120\n#00111:0100\n")
				.unwrap(),
		};
		let chart_id = format!("sha256/{}", bundle.chart_sha256());
		let url = server_runtime.block_on(support::spawn_http_server(
			vec![
				(format!("/assets/{asset_id}"), data),
				(format!("/bundles/{}", bundle.bundle_id()), bundle.to_json()),
				(format!("/charts/{chart_id}"), bundle.to_json()),
				(
					"/pack/header.json".into(),
					json!({"timestamp": "2026-07-15T00:00:00Z", "kind": "pack"})
						.to_string()
						.into_bytes(),
				),
				(
					"/pack/data.bbpack".into(),
					json!({
						"name": "Runtime Pack", "gamemode": "bms-7k",
						"updated": "2026-07-15T00:00:00Z", "tags": {}, "assets": {},
						"bundles": [{"id": bundle.bundle_id(), "desc": "Runtime", "tags": {}}]
					})
					.to_string()
					.into_bytes(),
				),
			],
			true,
		));
		Self {
			_server_runtime: server_runtime,
			url,
			bundle,
			asset_id,
		}
	}

	async fn exercise(&self) {
		let (_temp, store) = support::new_test_store_with(
			"runtime_independence",
			BackbeatConfig {
				servers: vec![ServerConfig {
					url: self.url.clone(),
				}],
				..BackbeatConfig::default()
			},
		);
		let bundle_id = self.bundle.bundle_id();
		let chart_id = format!("sha256/{}", self.bundle.chart_sha256())
			.parse()
			.unwrap();
		assert!(store.server_has_asset(self.asset_id).await.unwrap());
		assert!(store.server_has_bundle(bundle_id).await.unwrap());
		assert!(store.server_has_chart(&chart_id).await.unwrap());

		let url = format!("{}/pack", self.url);
		store.collection_fetch_header(&url).await.unwrap();
		store.collection_fetch_upsert(&url).await.unwrap();
		let caller_thread = std::thread::current().id();
		let progress = Rc::new(RefCell::new(Vec::new()));
		let report = store
			.collection_fetch_download_data(&url, |update| {
				assert_eq!(std::thread::current().id(), caller_thread);
				assert!(store.has_bundle(bundle_id).unwrap());
				progress.borrow_mut().push(update.current);
			})
			.await
			.unwrap();
		assert!(!report.has_failures(), "{report:?}");
		assert_eq!(report.bundles_downloaded, 1);
		assert_eq!(*progress.borrow(), vec![1]);
		assert!(store.has_asset(self.asset_id).unwrap());

		store.server_download_asset(self.asset_id).await.unwrap();
		store.server_download_bundle(bundle_id).await.unwrap();
		store.server_download_chart(&chart_id).await.unwrap();
		for _ in 0..64 {
			assert!(store.server_has_asset(self.asset_id).await.unwrap());
			assert!(store.has_bundle(bundle_id).unwrap());
		}
	}
}

#[test]
fn network_and_database_work_without_a_caller_tokio_runtime() {
	let fixture = Fixture::new();
	assert!(tokio::runtime::Handle::try_current().is_err());
	futures::executor::block_on(fixture.exercise());
}

#[test]
fn network_and_database_work_on_current_thread_without_io_or_timers() {
	let fixture = Fixture::new();
	let caller = tokio::runtime::Builder::new_current_thread()
		.build()
		.unwrap();
	caller.block_on(fixture.exercise());
}

#[test]
fn network_and_database_work_on_multi_thread_without_io_or_timers() {
	let fixture = Fixture::new();
	let caller = tokio::runtime::Builder::new_multi_thread()
		.worker_threads(1)
		.build()
		.unwrap();
	caller.block_on(fixture.exercise());
}

#[test]
fn synchronous_queue_keeps_downloading_without_a_caller_executor() {
	let fixture = Fixture::new();
	let (_temp, store) = support::new_test_store_with(
		"runtime_sync_queue",
		BackbeatConfig {
			servers: vec![ServerConfig {
				url: fixture.url.clone(),
			}],
			..BackbeatConfig::default()
		},
	);
	let bundle_id = store.import_bundle(&fixture.bundle).unwrap();
	store.bundle_download_assets(bundle_id).unwrap();
	let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
	while !store.has_asset(fixture.asset_id).unwrap() {
		assert!(
			std::time::Instant::now() < deadline,
			"queued download stalled"
		);
		std::thread::sleep(std::time::Duration::from_millis(5));
	}
}