feat: 实现资源服务系统及测试框架
新增资源服务系统核心功能,包括资源加载、缓存管理和异步处理机制。添加测试框架支持单元测试,包含以下主要变更: - 实现 AssetService 核心功能,支持资源同步/异步加载 - 添加 AssetCache 实现 LRU 缓存策略 - 引入测试框架,包含测试用例注册和断言机制 - 实现资源打包工具 asset_packer - 添加压缩库支持 (Zstd/LZ4/Zlib) - 完善文档说明资源服务系统设计 测试用例覆盖资源缓存、数据处理器、资源包管理等核心功能。文档详细说明系统架构和使用方法。
This commit is contained in:
parent
25c7c47872
commit
6a12bb5e2e
|
|
@ -0,0 +1,528 @@
|
||||||
|
# Extra2D 资源服务系统
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
Extra2D 资源服务系统提供了一套完整的资源管理解决方案,包括资源加载、缓存、打包和加密压缩功能。
|
||||||
|
|
||||||
|
### 主要特性
|
||||||
|
|
||||||
|
- **类型安全**:使用模板和强类型 ID 避免类型错误
|
||||||
|
- **自动缓存**:LRU 缓存策略,自动管理内存
|
||||||
|
- **异步加载**:后台线程加载,不阻塞主线程
|
||||||
|
- **资源打包**:支持压缩和加密的资源包格式
|
||||||
|
- **多格式支持**:纹理、字体、着色器、音频等
|
||||||
|
|
||||||
|
## 模块结构
|
||||||
|
|
||||||
|
```
|
||||||
|
extra2d/asset/
|
||||||
|
├── asset_types.h # 资源类型定义
|
||||||
|
├── asset.h # 资源基类
|
||||||
|
├── asset_handle.h # 资源句柄
|
||||||
|
├── asset_cache.h # 资源缓存
|
||||||
|
├── asset_loader.h # 资源加载器
|
||||||
|
├── asset_pack.h # 资源包
|
||||||
|
├── data_processor.h # 数据处理器
|
||||||
|
└── asset_service.h # 资源服务
|
||||||
|
```
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 1. 初始化资源服务
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <extra2d/extra2d.h>
|
||||||
|
|
||||||
|
using namespace extra2d;
|
||||||
|
|
||||||
|
// 创建资源服务
|
||||||
|
AssetService service;
|
||||||
|
service.init();
|
||||||
|
service.setRoot("assets");
|
||||||
|
|
||||||
|
// 注册加载器
|
||||||
|
service.registerLoader<TextureAsset>(AssetLoaderFactory::createTextureLoader());
|
||||||
|
service.registerLoader<FontAsset>(AssetLoaderFactory::createFontLoader());
|
||||||
|
service.registerLoader<ShaderAsset>(AssetLoaderFactory::createShaderLoader());
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 同步加载资源
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// 加载纹理
|
||||||
|
AssetHandle<TextureAsset> texture = service.load<TextureAsset>("sprites/player.png");
|
||||||
|
if (texture.valid()) {
|
||||||
|
int width = texture->width();
|
||||||
|
int height = texture->height();
|
||||||
|
const u8* data = texture->data();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 异步加载资源
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// 异步加载
|
||||||
|
service.loadAsync<TextureAsset>("sprites/background.png",
|
||||||
|
[](AssetHandle<TextureAsset> handle) {
|
||||||
|
if (handle.valid()) {
|
||||||
|
// 资源加载完成,在主线程处理
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 在主线程处理回调
|
||||||
|
service.process();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 使用资源包
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// 挂载资源包
|
||||||
|
service.mount("data.pak");
|
||||||
|
|
||||||
|
// 从资源包加载
|
||||||
|
auto texture = service.load<TextureAsset>("textures/hero.png");
|
||||||
|
|
||||||
|
// 卸载资源包
|
||||||
|
service.unmount("data.pak");
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 缓存管理
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// 设置缓存上限 (100MB)
|
||||||
|
service.setLimit(100 * 1024 * 1024);
|
||||||
|
|
||||||
|
// 获取缓存统计
|
||||||
|
CacheStats stats = service.stats();
|
||||||
|
std::cout << "缓存使用: " << stats.bytes << " 字节\n";
|
||||||
|
std::cout << "命中率: " << (stats.hitRate() * 100) << "%\n";
|
||||||
|
|
||||||
|
// 清理无引用资源
|
||||||
|
service.purge();
|
||||||
|
|
||||||
|
// 清空缓存
|
||||||
|
service.clear();
|
||||||
|
```
|
||||||
|
|
||||||
|
## API 参考
|
||||||
|
|
||||||
|
### AssetID
|
||||||
|
|
||||||
|
资源标识符,使用哈希值进行快速比较。
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
struct AssetID {
|
||||||
|
u64 hash; // 哈希值
|
||||||
|
std::string path; // 原始路径
|
||||||
|
|
||||||
|
explicit AssetID(const std::string& path);
|
||||||
|
bool valid() const;
|
||||||
|
bool operator==(const AssetID& other) const;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### AssetHandle<T>
|
||||||
|
|
||||||
|
类型安全的资源句柄,使用弱引用避免阻止资源回收。
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
template<typename T>
|
||||||
|
class AssetHandle {
|
||||||
|
public:
|
||||||
|
bool valid() const; // 检查是否有效
|
||||||
|
Ref<T> get() const; // 获取强引用
|
||||||
|
T* operator->() const; // 解引用
|
||||||
|
bool loaded() const; // 检查是否已加载
|
||||||
|
AssetState state() const; // 获取状态
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### AssetCache
|
||||||
|
|
||||||
|
LRU 缓存管理器。
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
class AssetCache {
|
||||||
|
public:
|
||||||
|
explicit AssetCache(size_t limit = 0);
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
AssetHandle<T> add(Ref<T> asset);
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
AssetHandle<T> get(const AssetID& id);
|
||||||
|
|
||||||
|
bool has(const AssetID& id) const;
|
||||||
|
bool remove(const AssetID& id);
|
||||||
|
|
||||||
|
void setLimit(size_t limit);
|
||||||
|
size_t bytes() const;
|
||||||
|
size_t count() const;
|
||||||
|
|
||||||
|
size_t purge(); // 清理无引用资源
|
||||||
|
void clear(); // 清空缓存
|
||||||
|
CacheStats stats() const;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### IAssetService
|
||||||
|
|
||||||
|
资源服务接口。
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
class IAssetService : public IService {
|
||||||
|
public:
|
||||||
|
// 同步加载
|
||||||
|
template<typename T>
|
||||||
|
AssetHandle<T> load(const std::string& path);
|
||||||
|
|
||||||
|
// 异步加载
|
||||||
|
template<typename T>
|
||||||
|
void loadAsync(const std::string& path, AssetLoadCallback<T> callback);
|
||||||
|
|
||||||
|
// 获取已缓存资源
|
||||||
|
template<typename T>
|
||||||
|
AssetHandle<T> get(const std::string& path);
|
||||||
|
|
||||||
|
// 预加载
|
||||||
|
template<typename T>
|
||||||
|
void preload(const std::string& path);
|
||||||
|
|
||||||
|
// 状态查询
|
||||||
|
bool isLoaded(const std::string& path) const;
|
||||||
|
bool isLoading(const std::string& path) const;
|
||||||
|
|
||||||
|
// 资源管理
|
||||||
|
void unload(const std::string& path);
|
||||||
|
void setLimit(size_t maxBytes);
|
||||||
|
size_t size() const;
|
||||||
|
void purge();
|
||||||
|
void clear();
|
||||||
|
CacheStats stats() const;
|
||||||
|
|
||||||
|
// 加载器注册
|
||||||
|
template<typename T>
|
||||||
|
void registerLoader(Unique<AssetLoader<T>> loader);
|
||||||
|
|
||||||
|
// 资源包管理
|
||||||
|
bool mount(const std::string& path);
|
||||||
|
void unmount(const std::string& path);
|
||||||
|
|
||||||
|
// 数据处理管道
|
||||||
|
void setPipe(DataPipe pipe);
|
||||||
|
|
||||||
|
// 根目录
|
||||||
|
void setRoot(const std::string& path);
|
||||||
|
std::string root() const;
|
||||||
|
|
||||||
|
// 处理异步回调
|
||||||
|
void process();
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### DataPipe
|
||||||
|
|
||||||
|
数据处理管道,支持链式调用。
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
class DataPipe {
|
||||||
|
public:
|
||||||
|
DataPipe& decrypt(const std::string& key, Decryptor::Type type = Decryptor::Type::XOR);
|
||||||
|
DataPipe& decompress(Compression algo);
|
||||||
|
DataPipe& encrypt(const std::string& key, Decryptor::Type type = Decryptor::Type::XOR);
|
||||||
|
DataPipe& compress(Compression algo, int level = 3);
|
||||||
|
DataPipe& add(Unique<DataProcessor> processor);
|
||||||
|
|
||||||
|
std::vector<u8> process(const std::vector<u8>& input);
|
||||||
|
void clear();
|
||||||
|
bool empty() const;
|
||||||
|
size_t size() const;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### AssetPackBuilder
|
||||||
|
|
||||||
|
资源包构建器。
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
class AssetPackBuilder {
|
||||||
|
public:
|
||||||
|
explicit AssetPackBuilder(Compression compression = Compression::None, int level = 3);
|
||||||
|
|
||||||
|
void add(const std::string& path, const std::vector<u8>& data);
|
||||||
|
void add(const std::string& path, std::vector<u8>&& data);
|
||||||
|
bool addFile(const std::string& filePath, const std::string& packPath = "");
|
||||||
|
size_t addDirectory(const std::string& dirPath, const std::string& prefix = "");
|
||||||
|
|
||||||
|
void setEncryption(const std::string& key, Decryptor::Type type = Decryptor::Type::XOR);
|
||||||
|
bool build(const std::string& outputPath);
|
||||||
|
|
||||||
|
void clear();
|
||||||
|
size_t count() const;
|
||||||
|
size_t totalOriginalSize() const;
|
||||||
|
size_t totalCompressedSize() const;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 资源类型
|
||||||
|
|
||||||
|
### TextureAsset
|
||||||
|
|
||||||
|
纹理资源,使用 stb_image 加载。
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
class TextureAsset : public Asset {
|
||||||
|
public:
|
||||||
|
AssetType type() const override { return AssetType::Texture; }
|
||||||
|
|
||||||
|
int width() const;
|
||||||
|
int height() const;
|
||||||
|
int channels() const;
|
||||||
|
const u8* data() const;
|
||||||
|
size_t dataSize() const;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### FontAsset
|
||||||
|
|
||||||
|
字体资源,支持 TrueType 字体。
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
class FontAsset : public Asset {
|
||||||
|
public:
|
||||||
|
AssetType type() const override { return AssetType::Font; }
|
||||||
|
|
||||||
|
float scaleForPixelHeight(float pixels) const;
|
||||||
|
const u8* data() const;
|
||||||
|
size_t dataSize() const;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### ShaderAsset
|
||||||
|
|
||||||
|
着色器资源。
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
class ShaderAsset : public Asset {
|
||||||
|
public:
|
||||||
|
AssetType type() const override { return AssetType::Shader; }
|
||||||
|
|
||||||
|
const std::string& vertexSource() const;
|
||||||
|
const std::string& fragmentSource() const;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### AudioAsset
|
||||||
|
|
||||||
|
音频资源。
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
class AudioAsset : public Asset {
|
||||||
|
public:
|
||||||
|
AssetType type() const override { return AssetType::Audio; }
|
||||||
|
|
||||||
|
AudioFormat format() const;
|
||||||
|
int channels() const;
|
||||||
|
int sampleRate() const;
|
||||||
|
int bitsPerSample() const;
|
||||||
|
float duration() const;
|
||||||
|
const u8* data() const;
|
||||||
|
size_t dataSize() const;
|
||||||
|
bool streaming() const;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### DataAsset
|
||||||
|
|
||||||
|
通用二进制数据。
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
class DataAsset : public Asset {
|
||||||
|
public:
|
||||||
|
AssetType type() const override { return AssetType::Data; }
|
||||||
|
|
||||||
|
const u8* data() const;
|
||||||
|
size_t size() const;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 资源打包工具
|
||||||
|
|
||||||
|
### 安装
|
||||||
|
|
||||||
|
```bash
|
||||||
|
xmake build asset_packer
|
||||||
|
```
|
||||||
|
|
||||||
|
### 用法
|
||||||
|
|
||||||
|
```
|
||||||
|
Extra2D 资源打包工具 v1.0.0
|
||||||
|
|
||||||
|
用法:
|
||||||
|
asset_packer create <output.pack> [options] <inputs...>
|
||||||
|
asset_packer list <pack file>
|
||||||
|
asset_packer extract <pack file> <output dir>
|
||||||
|
|
||||||
|
命令:
|
||||||
|
create 创建资源包
|
||||||
|
list 列出资源包内容
|
||||||
|
extract 提取资源包内容
|
||||||
|
|
||||||
|
选项:
|
||||||
|
-c, --compression <algo> 压缩算法 (none, zstd, lz4, zlib),默认 zstd
|
||||||
|
-l, --level <level> 压缩级别 (1-22),默认 3
|
||||||
|
-e, --encrypt <key> 加密密钥
|
||||||
|
-t, --encrypt-type <type> 加密类型 (xor, aes),默认 xor
|
||||||
|
-v, --verbose 详细输出
|
||||||
|
-h, --help 显示帮助
|
||||||
|
```
|
||||||
|
|
||||||
|
### 示例
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 创建资源包(使用 Zstd 压缩)
|
||||||
|
asset_packer create game.pak -c zstd -v assets/
|
||||||
|
|
||||||
|
# 创建加密资源包
|
||||||
|
asset_packer create game.pak -c zstd -e "secret_key" -t xor -v assets/
|
||||||
|
|
||||||
|
# 列出资源包内容
|
||||||
|
asset_packer list game.pak
|
||||||
|
|
||||||
|
# 提取资源包
|
||||||
|
asset_packer extract game.pak extracted/
|
||||||
|
|
||||||
|
# 使用不同压缩算法
|
||||||
|
asset_packer create game.pak -c lz4 -v assets/
|
||||||
|
asset_packer create game.pak -c zlib -l 9 -v assets/
|
||||||
|
```
|
||||||
|
|
||||||
|
## 压缩算法对比
|
||||||
|
|
||||||
|
| 算法 | 压缩率 | 压缩速度 | 解压速度 | 适用场景 |
|
||||||
|
|------|--------|----------|----------|----------|
|
||||||
|
| Zstd | 高 | 快 | 很快 | 通用推荐 |
|
||||||
|
| LZ4 | 中 | 很快 | 很快 | 实时解压 |
|
||||||
|
| Zlib | 高 | 中 | 中 | 兼容性好 |
|
||||||
|
| None | - | - | - | 已压缩资源 |
|
||||||
|
|
||||||
|
## 设计模式
|
||||||
|
|
||||||
|
### 策略模式 (AssetLoader)
|
||||||
|
|
||||||
|
不同资源类型使用不同的加载策略。
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
class AssetLoader<T> {
|
||||||
|
public:
|
||||||
|
virtual Ref<T> load(const std::string& path) = 0;
|
||||||
|
virtual Ref<T> loadFromMemory(const u8* data, size_t size) = 0;
|
||||||
|
virtual bool canLoad(const std::string& path) const = 0;
|
||||||
|
virtual AssetType type() const = 0;
|
||||||
|
virtual std::vector<std::string> extensions() const = 0;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 享元模式 (AssetCache)
|
||||||
|
|
||||||
|
共享资源实例,减少内存占用。
|
||||||
|
|
||||||
|
### 装饰器模式 (DataProcessor)
|
||||||
|
|
||||||
|
链式处理数据流。
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
DataPipe pipe;
|
||||||
|
pipe.encrypt("key")
|
||||||
|
.compress(Compression::Zstd);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 服务定位器模式
|
||||||
|
|
||||||
|
全局访问资源服务。
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
auto* service = ServiceLocator::get<IAssetService>();
|
||||||
|
```
|
||||||
|
|
||||||
|
## 线程安全
|
||||||
|
|
||||||
|
- `AssetCache` 使用读写锁 (`std::shared_mutex`)
|
||||||
|
- `AssetService` 使用读写锁保护资源映射
|
||||||
|
- 异步加载使用独立的工作线程
|
||||||
|
- 回调在主线程执行(需要调用 `process()`)
|
||||||
|
|
||||||
|
## 最佳实践
|
||||||
|
|
||||||
|
### 1. 资源路径约定
|
||||||
|
|
||||||
|
```
|
||||||
|
assets/
|
||||||
|
├── textures/
|
||||||
|
│ ├── sprites/
|
||||||
|
│ └── backgrounds/
|
||||||
|
├── fonts/
|
||||||
|
├── shaders/
|
||||||
|
└── audio/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 预加载关键资源
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// 游戏启动时预加载
|
||||||
|
service.preload<TextureAsset>("textures/loading.png");
|
||||||
|
service.preload<FontAsset>("fonts/main.ttf");
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 合理设置缓存上限
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// 根据目标平台设置
|
||||||
|
#ifdef MOBILE
|
||||||
|
service.setLimit(50 * 1024 * 1024); // 50MB
|
||||||
|
#else
|
||||||
|
service.setLimit(200 * 1024 * 1024); // 200MB
|
||||||
|
#endif
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 定期清理缓存
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// 场景切换时清理
|
||||||
|
void onSceneChange() {
|
||||||
|
service.purge();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 使用资源包减少文件数量
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// 将小文件打包成资源包
|
||||||
|
// 减少文件 I/O 操作,提高加载速度
|
||||||
|
service.mount("textures.pak");
|
||||||
|
service.mount("audio.pak");
|
||||||
|
```
|
||||||
|
|
||||||
|
## 错误处理
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
auto handle = service.load<TextureAsset>("missing.png");
|
||||||
|
if (!handle.valid()) {
|
||||||
|
// 资源加载失败
|
||||||
|
std::cerr << "Failed to load texture\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查加载状态
|
||||||
|
if (handle.state() == AssetState::Failed) {
|
||||||
|
// 处理失败情况
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 版本历史
|
||||||
|
|
||||||
|
- **v1.0.0** - 初始版本
|
||||||
|
- 资源加载和缓存
|
||||||
|
- 资源打包和加密
|
||||||
|
- 异步加载支持
|
||||||
|
- 多种压缩算法
|
||||||
|
|
@ -47,7 +47,7 @@ public:
|
||||||
*/
|
*/
|
||||||
template <typename T> AssetHandle<T> load(const std::string &path) {
|
template <typename T> AssetHandle<T> load(const std::string &path) {
|
||||||
static_assert(std::is_base_of_v<Asset, T>, "T must derive from Asset");
|
static_assert(std::is_base_of_v<Asset, T>, "T must derive from Asset");
|
||||||
return loadImpl(AssetID(path), typeid(T));
|
return AssetHandle<T>(loadImpl(AssetID(path), typeid(T)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -73,7 +73,7 @@ public:
|
||||||
*/
|
*/
|
||||||
template <typename T> AssetHandle<T> get(const std::string &path) {
|
template <typename T> AssetHandle<T> get(const std::string &path) {
|
||||||
static_assert(std::is_base_of_v<Asset, T>, "T must derive from Asset");
|
static_assert(std::is_base_of_v<Asset, T>, "T must derive from Asset");
|
||||||
return getImpl(AssetID(path), typeid(T));
|
return AssetHandle<T>(getImpl(AssetID(path), typeid(T)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,22 @@ void AssetService::setLimit(size_t maxBytes) { cache_->setLimit(maxBytes); }
|
||||||
|
|
||||||
size_t AssetService::size() const { return cache_->size(); }
|
size_t AssetService::size() const { return cache_->size(); }
|
||||||
|
|
||||||
void AssetService::purge() { cache_->purge(); }
|
void AssetService::purge() {
|
||||||
|
std::unique_lock<std::shared_mutex> lock(mutex_);
|
||||||
|
|
||||||
|
std::vector<AssetID> toRemove;
|
||||||
|
for (const auto& [id, loaded] : assets_) {
|
||||||
|
if (loaded.asset.use_count() <= 2) {
|
||||||
|
toRemove.push_back(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const auto& id : toRemove) {
|
||||||
|
assets_.erase(id);
|
||||||
|
states_.erase(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
cache_->purge();
|
||||||
|
}
|
||||||
|
|
||||||
void AssetService::clear() {
|
void AssetService::clear() {
|
||||||
std::unique_lock<std::shared_mutex> lock(mutex_);
|
std::unique_lock<std::shared_mutex> lock(mutex_);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,307 @@
|
||||||
|
/**
|
||||||
|
* @file test_asset_cache.cpp
|
||||||
|
* @brief AssetCache 单元测试
|
||||||
|
*
|
||||||
|
* 测试资源缓存的添加、获取、淘汰等功能。
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "test_framework.h"
|
||||||
|
#include <extra2d/asset/asset_cache.h>
|
||||||
|
#include <extra2d/asset/asset.h>
|
||||||
|
|
||||||
|
using namespace extra2d;
|
||||||
|
using namespace extra2d::test;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 测试辅助类
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 测试用资源类
|
||||||
|
*/
|
||||||
|
class TestAsset : public Asset {
|
||||||
|
public:
|
||||||
|
AssetType type() const override { return AssetType::Data; }
|
||||||
|
|
||||||
|
bool loaded() const override {
|
||||||
|
return state_.load(std::memory_order_acquire) == AssetState::Loaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t memSize() const override { return dataSize_; }
|
||||||
|
|
||||||
|
void setData(size_t size) {
|
||||||
|
dataSize_ = size;
|
||||||
|
setState(AssetState::Loaded);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Ref<TestAsset> create(const std::string& path, size_t size = 100) {
|
||||||
|
auto asset = std::make_shared<TestAsset>();
|
||||||
|
asset->setId(AssetID(path));
|
||||||
|
asset->setPath(path);
|
||||||
|
asset->setData(size);
|
||||||
|
return asset;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
size_t dataSize_ = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 基本功能测试
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST(AssetCache, Create) {
|
||||||
|
AssetCache cache;
|
||||||
|
|
||||||
|
TEST_ASSERT_EQ(0u, cache.count());
|
||||||
|
TEST_ASSERT_EQ(0u, cache.bytes());
|
||||||
|
TEST_ASSERT_EQ(0u, cache.limit());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(AssetCache, CreateWithLimit) {
|
||||||
|
AssetCache cache(1024 * 1024);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQ(1024 * 1024, cache.limit());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(AssetCache, AddAsset) {
|
||||||
|
AssetCache cache;
|
||||||
|
|
||||||
|
auto asset = TestAsset::create("test.asset", 100);
|
||||||
|
auto handle = cache.add(asset);
|
||||||
|
|
||||||
|
TEST_ASSERT_TRUE(handle.valid());
|
||||||
|
TEST_ASSERT_EQ(1u, cache.count());
|
||||||
|
TEST_ASSERT_EQ(100u, cache.bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(AssetCache, AddNullAsset) {
|
||||||
|
AssetCache cache;
|
||||||
|
|
||||||
|
Ref<TestAsset> nullAsset;
|
||||||
|
auto handle = cache.add(nullAsset);
|
||||||
|
|
||||||
|
TEST_ASSERT_FALSE(handle.valid());
|
||||||
|
TEST_ASSERT_EQ(0u, cache.count());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(AssetCache, GetAsset) {
|
||||||
|
AssetCache cache;
|
||||||
|
|
||||||
|
auto asset = TestAsset::create("test.asset", 200);
|
||||||
|
cache.add(asset);
|
||||||
|
|
||||||
|
auto handle = cache.get<TestAsset>(AssetID("test.asset"));
|
||||||
|
|
||||||
|
TEST_ASSERT_TRUE(handle.valid());
|
||||||
|
TEST_ASSERT_TRUE(handle.get() != nullptr);
|
||||||
|
TEST_ASSERT_EQ(asset.get(), handle.get().get());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(AssetCache, GetNonExistent) {
|
||||||
|
AssetCache cache;
|
||||||
|
|
||||||
|
auto handle = cache.get<TestAsset>(AssetID("nonexistent.asset"));
|
||||||
|
|
||||||
|
TEST_ASSERT_FALSE(handle.valid());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(AssetCache, HasAsset) {
|
||||||
|
AssetCache cache;
|
||||||
|
|
||||||
|
auto asset = TestAsset::create("test.asset", 100);
|
||||||
|
cache.add(asset);
|
||||||
|
|
||||||
|
TEST_ASSERT_TRUE(cache.has(AssetID("test.asset")));
|
||||||
|
TEST_ASSERT_FALSE(cache.has(AssetID("other.asset")));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(AssetCache, RemoveAsset) {
|
||||||
|
AssetCache cache;
|
||||||
|
|
||||||
|
auto asset = TestAsset::create("test.asset", 100);
|
||||||
|
cache.add(asset);
|
||||||
|
|
||||||
|
TEST_ASSERT_TRUE(cache.remove(AssetID("test.asset")));
|
||||||
|
TEST_ASSERT_EQ(0u, cache.count());
|
||||||
|
TEST_ASSERT_FALSE(cache.has(AssetID("test.asset")));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(AssetCache, RemoveNonExistent) {
|
||||||
|
AssetCache cache;
|
||||||
|
|
||||||
|
TEST_ASSERT_FALSE(cache.remove(AssetID("nonexistent.asset")));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 缓存统计测试
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST(AssetCache, Stats_Initial) {
|
||||||
|
AssetCache cache;
|
||||||
|
|
||||||
|
auto stats = cache.stats();
|
||||||
|
|
||||||
|
TEST_ASSERT_EQ(0u, stats.hits);
|
||||||
|
TEST_ASSERT_EQ(0u, stats.misses);
|
||||||
|
TEST_ASSERT_EQ(0u, stats.count);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(AssetCache, Stats_HitMiss) {
|
||||||
|
AssetCache cache;
|
||||||
|
|
||||||
|
auto asset = TestAsset::create("test.asset", 100);
|
||||||
|
cache.add(asset);
|
||||||
|
|
||||||
|
cache.get<TestAsset>(AssetID("test.asset"));
|
||||||
|
cache.get<TestAsset>(AssetID("nonexistent.asset"));
|
||||||
|
|
||||||
|
auto stats = cache.stats();
|
||||||
|
|
||||||
|
TEST_ASSERT_EQ(1u, stats.hits);
|
||||||
|
TEST_ASSERT_EQ(1u, stats.misses);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(AssetCache, Stats_Reset) {
|
||||||
|
AssetCache cache;
|
||||||
|
|
||||||
|
auto asset = TestAsset::create("test.asset", 100);
|
||||||
|
cache.add(asset);
|
||||||
|
|
||||||
|
cache.get<TestAsset>(AssetID("test.asset"));
|
||||||
|
cache.get<TestAsset>(AssetID("nonexistent.asset"));
|
||||||
|
|
||||||
|
cache.resetStats();
|
||||||
|
|
||||||
|
auto stats = cache.stats();
|
||||||
|
|
||||||
|
TEST_ASSERT_EQ(0u, stats.hits);
|
||||||
|
TEST_ASSERT_EQ(0u, stats.misses);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 内存限制测试
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST(AssetCache, SetLimit) {
|
||||||
|
AssetCache cache;
|
||||||
|
|
||||||
|
cache.setLimit(500);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQ(500u, cache.limit());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(AssetCache, MemoryTracking) {
|
||||||
|
AssetCache cache;
|
||||||
|
|
||||||
|
auto asset1 = TestAsset::create("asset1", 100);
|
||||||
|
auto asset2 = TestAsset::create("asset2", 200);
|
||||||
|
|
||||||
|
cache.add(asset1);
|
||||||
|
cache.add(asset2);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQ(300u, cache.bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(AssetCache, Clear) {
|
||||||
|
AssetCache cache;
|
||||||
|
|
||||||
|
auto asset1 = TestAsset::create("asset1", 100);
|
||||||
|
auto asset2 = TestAsset::create("asset2", 200);
|
||||||
|
|
||||||
|
cache.add(asset1);
|
||||||
|
cache.add(asset2);
|
||||||
|
|
||||||
|
cache.clear();
|
||||||
|
|
||||||
|
TEST_ASSERT_EQ(0u, cache.count());
|
||||||
|
TEST_ASSERT_EQ(0u, cache.bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// LRU 测试
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST(AssetCache, LRU_AccessOrder) {
|
||||||
|
AssetCache cache;
|
||||||
|
|
||||||
|
auto asset1 = TestAsset::create("asset1", 100);
|
||||||
|
auto asset2 = TestAsset::create("asset2", 100);
|
||||||
|
auto asset3 = TestAsset::create("asset3", 100);
|
||||||
|
|
||||||
|
cache.add(asset1);
|
||||||
|
cache.add(asset2);
|
||||||
|
cache.add(asset3);
|
||||||
|
|
||||||
|
cache.get<TestAsset>(AssetID("asset1"));
|
||||||
|
|
||||||
|
TEST_ASSERT_TRUE(cache.has(AssetID("asset1")));
|
||||||
|
TEST_ASSERT_TRUE(cache.has(AssetID("asset2")));
|
||||||
|
TEST_ASSERT_TRUE(cache.has(AssetID("asset3")));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Purge 测试
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST(AssetCache, Purge_NoReferences) {
|
||||||
|
AssetCache cache;
|
||||||
|
|
||||||
|
{
|
||||||
|
auto asset = TestAsset::create("test.asset", 100);
|
||||||
|
cache.add(asset);
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t purged = cache.purge();
|
||||||
|
|
||||||
|
TEST_ASSERT_EQ(1u, purged);
|
||||||
|
TEST_ASSERT_EQ(0u, cache.count());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(AssetCache, Purge_WithReferences) {
|
||||||
|
AssetCache cache;
|
||||||
|
|
||||||
|
auto asset = TestAsset::create("test.asset", 100);
|
||||||
|
auto handle = cache.add(asset);
|
||||||
|
|
||||||
|
size_t purged = cache.purge();
|
||||||
|
|
||||||
|
TEST_ASSERT_EQ(0u, purged);
|
||||||
|
TEST_ASSERT_EQ(1u, cache.count());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 多资源测试
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST(AssetCache, MultipleAssets) {
|
||||||
|
AssetCache cache;
|
||||||
|
|
||||||
|
for (int i = 0; i < 10; ++i) {
|
||||||
|
auto asset = TestAsset::create("asset" + std::to_string(i), 100);
|
||||||
|
cache.add(asset);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_ASSERT_EQ(10u, cache.count());
|
||||||
|
TEST_ASSERT_EQ(1000u, cache.bytes());
|
||||||
|
|
||||||
|
for (int i = 0; i < 10; ++i) {
|
||||||
|
TEST_ASSERT_TRUE(cache.has(AssetID("asset" + std::to_string(i))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(AssetCache, RemoveMultiple) {
|
||||||
|
AssetCache cache;
|
||||||
|
|
||||||
|
for (int i = 0; i < 5; ++i) {
|
||||||
|
auto asset = TestAsset::create("asset" + std::to_string(i), 100);
|
||||||
|
cache.add(asset);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < 3; ++i) {
|
||||||
|
cache.remove(AssetID("asset" + std::to_string(i)));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_ASSERT_EQ(2u, cache.count());
|
||||||
|
TEST_ASSERT_EQ(200u, cache.bytes());
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,427 @@
|
||||||
|
/**
|
||||||
|
* @file test_asset_pack.cpp
|
||||||
|
* @brief AssetPack 单元测试
|
||||||
|
*
|
||||||
|
* 测试资源包的创建、读取和管理功能。
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "test_framework.h"
|
||||||
|
#include <extra2d/asset/asset_pack.h>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
|
||||||
|
using namespace extra2d;
|
||||||
|
using namespace extra2d::test;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 测试辅助函数
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
std::string testDir = "test_temp";
|
||||||
|
|
||||||
|
void setupTestDir() { std::filesystem::create_directories(testDir); }
|
||||||
|
|
||||||
|
void cleanupTestDir() { std::filesystem::remove_all(testDir); }
|
||||||
|
|
||||||
|
std::string getTestPath(const std::string &name) {
|
||||||
|
return testDir + "/" + name;
|
||||||
|
}
|
||||||
|
|
||||||
|
void writeTestFile(const std::string &path, const std::vector<u8> &data) {
|
||||||
|
std::ofstream file(path, std::ios::binary);
|
||||||
|
file.write(reinterpret_cast<const char *>(data.data()), data.size());
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// AssetPackBuilder 测试
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST(AssetPackBuilder, Create) {
|
||||||
|
AssetPackBuilder builder;
|
||||||
|
|
||||||
|
TEST_ASSERT_EQ(0u, builder.count());
|
||||||
|
TEST_ASSERT_EQ(0u, builder.totalOriginalSize());
|
||||||
|
TEST_ASSERT_EQ(0u, builder.totalCompressedSize());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(AssetPackBuilder, AddData) {
|
||||||
|
AssetPackBuilder builder;
|
||||||
|
|
||||||
|
std::vector<u8> data = createTestData(1024);
|
||||||
|
builder.add("test.bin", data);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQ(1u, builder.count());
|
||||||
|
TEST_ASSERT_EQ(1024u, builder.totalOriginalSize());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(AssetPackBuilder, AddMultipleData) {
|
||||||
|
AssetPackBuilder builder;
|
||||||
|
|
||||||
|
builder.add("file1.bin", createTestData(100));
|
||||||
|
builder.add("file2.bin", createTestData(200));
|
||||||
|
builder.add("file3.bin", createTestData(300));
|
||||||
|
|
||||||
|
TEST_ASSERT_EQ(3u, builder.count());
|
||||||
|
TEST_ASSERT_EQ(600u, builder.totalOriginalSize());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(AssetPackBuilder, AddMovedData) {
|
||||||
|
AssetPackBuilder builder;
|
||||||
|
|
||||||
|
std::vector<u8> data = createTestData(512);
|
||||||
|
builder.add("moved.bin", std::move(data));
|
||||||
|
|
||||||
|
TEST_ASSERT_EQ(1u, builder.count());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(AssetPackBuilder, Clear) {
|
||||||
|
AssetPackBuilder builder;
|
||||||
|
|
||||||
|
builder.add("test.bin", createTestData(100));
|
||||||
|
builder.clear();
|
||||||
|
|
||||||
|
TEST_ASSERT_EQ(0u, builder.count());
|
||||||
|
TEST_ASSERT_EQ(0u, builder.totalOriginalSize());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(AssetPackBuilder, BuildSimple) {
|
||||||
|
setupTestDir();
|
||||||
|
|
||||||
|
AssetPackBuilder builder;
|
||||||
|
builder.add("test.bin", createTestData(256));
|
||||||
|
|
||||||
|
std::string packPath = getTestPath("simple.pack");
|
||||||
|
bool result = builder.build(packPath);
|
||||||
|
|
||||||
|
TEST_ASSERT_TRUE(result);
|
||||||
|
TEST_ASSERT_TRUE(std::filesystem::exists(packPath));
|
||||||
|
|
||||||
|
cleanupTestDir();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(AssetPackBuilder, BuildMultipleFiles) {
|
||||||
|
setupTestDir();
|
||||||
|
|
||||||
|
AssetPackBuilder builder;
|
||||||
|
builder.add("file1.bin", createTestData(100, 1));
|
||||||
|
builder.add("file2.bin", createTestData(200, 2));
|
||||||
|
builder.add("file3.bin", createTestData(300, 3));
|
||||||
|
|
||||||
|
std::string packPath = getTestPath("multi.pack");
|
||||||
|
bool result = builder.build(packPath);
|
||||||
|
|
||||||
|
TEST_ASSERT_TRUE(result);
|
||||||
|
TEST_ASSERT_TRUE(std::filesystem::exists(packPath));
|
||||||
|
|
||||||
|
cleanupTestDir();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(AssetPackBuilder, SetEncryption) {
|
||||||
|
AssetPackBuilder builder;
|
||||||
|
builder.setEncryption("secret-key", Decryptor::Type::XOR);
|
||||||
|
|
||||||
|
builder.add("encrypted.bin", createTestData(128));
|
||||||
|
|
||||||
|
TEST_ASSERT_EQ(1u, builder.count());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// AssetPack 测试
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST(AssetPack, Create) {
|
||||||
|
AssetPack pack;
|
||||||
|
|
||||||
|
TEST_ASSERT_FALSE(pack.isOpen());
|
||||||
|
TEST_ASSERT_EQ(0u, pack.count());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(AssetPack, OpenNonExistent) {
|
||||||
|
AssetPack pack;
|
||||||
|
|
||||||
|
bool result = pack.open("nonexistent.pack");
|
||||||
|
|
||||||
|
TEST_ASSERT_FALSE(result);
|
||||||
|
TEST_ASSERT_FALSE(pack.isOpen());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(AssetPack, OpenAndClose) {
|
||||||
|
setupTestDir();
|
||||||
|
|
||||||
|
AssetPackBuilder builder;
|
||||||
|
builder.add("test.bin", createTestData(100));
|
||||||
|
builder.build(getTestPath("test.pack"));
|
||||||
|
|
||||||
|
AssetPack pack;
|
||||||
|
bool opened = pack.open(getTestPath("test.pack"));
|
||||||
|
|
||||||
|
TEST_ASSERT_TRUE(opened);
|
||||||
|
TEST_ASSERT_TRUE(pack.isOpen());
|
||||||
|
|
||||||
|
pack.close();
|
||||||
|
|
||||||
|
TEST_ASSERT_FALSE(pack.isOpen());
|
||||||
|
|
||||||
|
cleanupTestDir();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(AssetPack, ReadAsset) {
|
||||||
|
setupTestDir();
|
||||||
|
|
||||||
|
std::vector<u8> originalData = createTestData(512);
|
||||||
|
|
||||||
|
AssetPackBuilder builder;
|
||||||
|
builder.add("data.bin", originalData);
|
||||||
|
builder.build(getTestPath("read.pack"));
|
||||||
|
|
||||||
|
AssetPack pack;
|
||||||
|
pack.open(getTestPath("read.pack"));
|
||||||
|
|
||||||
|
std::vector<u8> readData = pack.read("data.bin");
|
||||||
|
|
||||||
|
TEST_ASSERT_TRUE(dataEquals(originalData, readData));
|
||||||
|
|
||||||
|
pack.close();
|
||||||
|
cleanupTestDir();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(AssetPack, HasAsset) {
|
||||||
|
setupTestDir();
|
||||||
|
|
||||||
|
AssetPackBuilder builder;
|
||||||
|
builder.add("exists.bin", createTestData(100));
|
||||||
|
builder.build(getTestPath("has.pack"));
|
||||||
|
|
||||||
|
AssetPack pack;
|
||||||
|
pack.open(getTestPath("has.pack"));
|
||||||
|
|
||||||
|
TEST_ASSERT_TRUE(pack.has("exists.bin"));
|
||||||
|
TEST_ASSERT_FALSE(pack.has("not_exists.bin"));
|
||||||
|
|
||||||
|
pack.close();
|
||||||
|
cleanupTestDir();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(AssetPack, GetAssets) {
|
||||||
|
setupTestDir();
|
||||||
|
|
||||||
|
AssetPackBuilder builder;
|
||||||
|
builder.add("file1.bin", createTestData(100));
|
||||||
|
builder.add("file2.bin", createTestData(100));
|
||||||
|
builder.add("file3.bin", createTestData(100));
|
||||||
|
builder.build(getTestPath("assets.pack"));
|
||||||
|
|
||||||
|
AssetPack pack;
|
||||||
|
pack.open(getTestPath("assets.pack"));
|
||||||
|
|
||||||
|
std::vector<AssetID> assets = pack.assets();
|
||||||
|
|
||||||
|
TEST_ASSERT_EQ(3u, assets.size());
|
||||||
|
|
||||||
|
pack.close();
|
||||||
|
cleanupTestDir();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(AssetPack, MoveSemantics) {
|
||||||
|
setupTestDir();
|
||||||
|
|
||||||
|
AssetPackBuilder builder;
|
||||||
|
builder.add("test.bin", createTestData(100));
|
||||||
|
builder.build(getTestPath("move.pack"));
|
||||||
|
|
||||||
|
AssetPack pack1;
|
||||||
|
pack1.open(getTestPath("move.pack"));
|
||||||
|
|
||||||
|
AssetPack pack2 = std::move(pack1);
|
||||||
|
|
||||||
|
TEST_ASSERT_TRUE(pack2.isOpen());
|
||||||
|
TEST_ASSERT_FALSE(pack1.isOpen());
|
||||||
|
|
||||||
|
pack2.close();
|
||||||
|
cleanupTestDir();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// PackManager 测试
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST(PackManager, Create) {
|
||||||
|
PackManager manager;
|
||||||
|
|
||||||
|
TEST_ASSERT_EQ(0u, manager.count());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(PackManager, MountUnmount) {
|
||||||
|
setupTestDir();
|
||||||
|
|
||||||
|
AssetPackBuilder builder;
|
||||||
|
builder.add("test.bin", createTestData(100));
|
||||||
|
builder.build(getTestPath("manager.pack"));
|
||||||
|
|
||||||
|
PackManager manager;
|
||||||
|
bool mounted = manager.mount(getTestPath("manager.pack"));
|
||||||
|
|
||||||
|
TEST_ASSERT_TRUE(mounted);
|
||||||
|
TEST_ASSERT_EQ(1u, manager.count());
|
||||||
|
|
||||||
|
manager.unmount(getTestPath("manager.pack"));
|
||||||
|
|
||||||
|
TEST_ASSERT_EQ(0u, manager.count());
|
||||||
|
|
||||||
|
cleanupTestDir();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(PackManager, MountNonExistent) {
|
||||||
|
PackManager manager;
|
||||||
|
|
||||||
|
bool mounted = manager.mount("nonexistent.pack");
|
||||||
|
|
||||||
|
TEST_ASSERT_FALSE(mounted);
|
||||||
|
TEST_ASSERT_EQ(0u, manager.count());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(PackManager, HasAsset) {
|
||||||
|
setupTestDir();
|
||||||
|
|
||||||
|
AssetPackBuilder builder;
|
||||||
|
builder.add("resource.bin", createTestData(100));
|
||||||
|
builder.build(getTestPath("has.pack"));
|
||||||
|
|
||||||
|
PackManager manager;
|
||||||
|
manager.mount(getTestPath("has.pack"));
|
||||||
|
|
||||||
|
TEST_ASSERT_TRUE(manager.has("resource.bin"));
|
||||||
|
TEST_ASSERT_FALSE(manager.has("missing.bin"));
|
||||||
|
|
||||||
|
manager.unmountAll();
|
||||||
|
cleanupTestDir();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(PackManager, ReadAsset) {
|
||||||
|
setupTestDir();
|
||||||
|
|
||||||
|
std::vector<u8> originalData = createTestData(256);
|
||||||
|
|
||||||
|
AssetPackBuilder builder;
|
||||||
|
builder.add("data.bin", originalData);
|
||||||
|
builder.build(getTestPath("read.pack"));
|
||||||
|
|
||||||
|
PackManager manager;
|
||||||
|
manager.mount(getTestPath("read.pack"));
|
||||||
|
|
||||||
|
std::vector<u8> readData = manager.read("data.bin");
|
||||||
|
|
||||||
|
TEST_ASSERT_TRUE(dataEquals(originalData, readData));
|
||||||
|
|
||||||
|
manager.unmountAll();
|
||||||
|
cleanupTestDir();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(PackManager, MultiplePacks) {
|
||||||
|
setupTestDir();
|
||||||
|
|
||||||
|
AssetPackBuilder builder1;
|
||||||
|
builder1.add("pack1/file1.bin", createTestData(100));
|
||||||
|
builder1.build(getTestPath("pack1.pack"));
|
||||||
|
|
||||||
|
AssetPackBuilder builder2;
|
||||||
|
builder2.add("pack2/file2.bin", createTestData(200));
|
||||||
|
builder2.build(getTestPath("pack2.pack"));
|
||||||
|
|
||||||
|
PackManager manager;
|
||||||
|
manager.mount(getTestPath("pack1.pack"));
|
||||||
|
manager.mount(getTestPath("pack2.pack"));
|
||||||
|
|
||||||
|
TEST_ASSERT_EQ(2u, manager.count());
|
||||||
|
TEST_ASSERT_TRUE(manager.has("pack1/file1.bin"));
|
||||||
|
TEST_ASSERT_TRUE(manager.has("pack2/file2.bin"));
|
||||||
|
|
||||||
|
manager.unmountAll();
|
||||||
|
cleanupTestDir();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(PackManager, UnmountAll) {
|
||||||
|
setupTestDir();
|
||||||
|
|
||||||
|
AssetPackBuilder builder;
|
||||||
|
builder.add("test.bin", createTestData(100));
|
||||||
|
builder.build(getTestPath("unmount.pack"));
|
||||||
|
|
||||||
|
PackManager manager;
|
||||||
|
manager.mount(getTestPath("unmount.pack"));
|
||||||
|
|
||||||
|
manager.unmountAll();
|
||||||
|
|
||||||
|
TEST_ASSERT_EQ(0u, manager.count());
|
||||||
|
|
||||||
|
cleanupTestDir();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(PackManager, AllAssets) {
|
||||||
|
setupTestDir();
|
||||||
|
|
||||||
|
AssetPackBuilder builder;
|
||||||
|
builder.add("file1.bin", createTestData(100));
|
||||||
|
builder.add("file2.bin", createTestData(100));
|
||||||
|
builder.build(getTestPath("all.pack"));
|
||||||
|
|
||||||
|
PackManager manager;
|
||||||
|
manager.mount(getTestPath("all.pack"));
|
||||||
|
|
||||||
|
std::vector<AssetID> assets = manager.allAssets();
|
||||||
|
|
||||||
|
TEST_ASSERT_EQ(2u, assets.size());
|
||||||
|
|
||||||
|
manager.unmountAll();
|
||||||
|
cleanupTestDir();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(PackManager, MountedPacks) {
|
||||||
|
setupTestDir();
|
||||||
|
|
||||||
|
AssetPackBuilder builder;
|
||||||
|
builder.add("test.bin", createTestData(100));
|
||||||
|
builder.build(getTestPath("mounted.pack"));
|
||||||
|
|
||||||
|
PackManager manager;
|
||||||
|
manager.mount(getTestPath("mounted.pack"));
|
||||||
|
|
||||||
|
std::vector<std::string> mounted = manager.mountedPacks();
|
||||||
|
|
||||||
|
TEST_ASSERT_EQ(1u, mounted.size());
|
||||||
|
|
||||||
|
manager.unmountAll();
|
||||||
|
cleanupTestDir();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 加密资源包测试
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST(AssetPack, EncryptedPack) {
|
||||||
|
setupTestDir();
|
||||||
|
|
||||||
|
std::vector<u8> originalData = createTestData(256);
|
||||||
|
std::string key = "encryption-key";
|
||||||
|
|
||||||
|
AssetPackBuilder builder(Compression::None);
|
||||||
|
builder.setEncryption(key, Decryptor::Type::XOR);
|
||||||
|
builder.add("encrypted.bin", originalData);
|
||||||
|
builder.build(getTestPath("encrypted.pack"));
|
||||||
|
|
||||||
|
AssetPack pack;
|
||||||
|
pack.open(getTestPath("encrypted.pack"));
|
||||||
|
|
||||||
|
DataPipe pipe;
|
||||||
|
pipe.decrypt(key, Decryptor::Type::XOR);
|
||||||
|
pack.setPipe(std::move(pipe));
|
||||||
|
|
||||||
|
std::vector<u8> readData = pack.read("encrypted.bin");
|
||||||
|
|
||||||
|
TEST_ASSERT_TRUE(dataEquals(originalData, readData));
|
||||||
|
|
||||||
|
pack.close();
|
||||||
|
cleanupTestDir();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,500 @@
|
||||||
|
/**
|
||||||
|
* @file test_asset_service.cpp
|
||||||
|
* @brief AssetService 单元测试
|
||||||
|
*
|
||||||
|
* 测试资源服务的加载、缓存、异步加载等功能。
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "test_framework.h"
|
||||||
|
#include <extra2d/services/asset_service.h>
|
||||||
|
#include <extra2d/asset/asset_loader.h>
|
||||||
|
#include <chrono>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
using namespace extra2d;
|
||||||
|
using namespace extra2d::test;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 测试辅助函数
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
std::string testDir = "test_temp_service";
|
||||||
|
|
||||||
|
void setupTestDir() {
|
||||||
|
std::filesystem::create_directories(testDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
void cleanupTestDir() {
|
||||||
|
std::filesystem::remove_all(testDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string getTestPath(const std::string& name) {
|
||||||
|
return testDir + "/" + name;
|
||||||
|
}
|
||||||
|
|
||||||
|
void writeTestFile(const std::string& path, const std::vector<u8>& data) {
|
||||||
|
std::ofstream file(path, std::ios::binary);
|
||||||
|
file.write(reinterpret_cast<const char*>(data.data()), data.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 测试用资源类和加载器
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 测试用资源类
|
||||||
|
*/
|
||||||
|
class TestResource : public Asset {
|
||||||
|
public:
|
||||||
|
AssetType type() const override { return AssetType::Data; }
|
||||||
|
|
||||||
|
bool loaded() const override {
|
||||||
|
return state_.load(std::memory_order_acquire) == AssetState::Loaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t memSize() const override { return data_.size(); }
|
||||||
|
|
||||||
|
const std::vector<u8>& data() const { return data_; }
|
||||||
|
|
||||||
|
void setData(const std::vector<u8>& data) {
|
||||||
|
data_ = data;
|
||||||
|
setState(AssetState::Loaded);
|
||||||
|
}
|
||||||
|
|
||||||
|
void initId(const std::string& path) {
|
||||||
|
setId(AssetID(path));
|
||||||
|
setPath(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<u8> data_;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 测试用资源加载器
|
||||||
|
*/
|
||||||
|
class TestResourceLoader : public AssetLoader<TestResource> {
|
||||||
|
public:
|
||||||
|
Ref<TestResource> load(const std::string& path) override {
|
||||||
|
std::ifstream file(path, std::ios::binary | std::ios::ate);
|
||||||
|
if (!file.is_open()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t size = static_cast<size_t>(file.tellg());
|
||||||
|
file.seekg(0);
|
||||||
|
|
||||||
|
std::vector<u8> data(size);
|
||||||
|
file.read(reinterpret_cast<char*>(data.data()), size);
|
||||||
|
|
||||||
|
auto resource = std::make_shared<TestResource>();
|
||||||
|
resource->initId(path);
|
||||||
|
resource->setData(data);
|
||||||
|
|
||||||
|
return resource;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ref<TestResource> loadFromMemory(const u8* data, size_t size) override {
|
||||||
|
auto resource = std::make_shared<TestResource>();
|
||||||
|
resource->setData(std::vector<u8>(data, data + size));
|
||||||
|
return resource;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool canLoad(const std::string& path) const override {
|
||||||
|
return path.find(".test") != std::string::npos;
|
||||||
|
}
|
||||||
|
|
||||||
|
AssetType type() const override { return AssetType::Data; }
|
||||||
|
|
||||||
|
std::vector<std::string> extensions() const override {
|
||||||
|
return {".test"};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// AssetService 基本测试
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST(AssetService, Create) {
|
||||||
|
AssetService service;
|
||||||
|
|
||||||
|
TEST_ASSERT_FALSE(service.isLoaded("nonexistent"));
|
||||||
|
TEST_ASSERT_FALSE(service.isLoading("nonexistent"));
|
||||||
|
TEST_ASSERT_EQ(0u, service.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(AssetService, InitShutdown) {
|
||||||
|
AssetService service;
|
||||||
|
|
||||||
|
bool initResult = service.init();
|
||||||
|
|
||||||
|
TEST_ASSERT_TRUE(initResult);
|
||||||
|
|
||||||
|
service.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(AssetService, SetRoot) {
|
||||||
|
AssetService service;
|
||||||
|
service.init();
|
||||||
|
|
||||||
|
service.setRoot("assets");
|
||||||
|
|
||||||
|
TEST_ASSERT_EQ("assets", service.root());
|
||||||
|
|
||||||
|
service.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(AssetService, SetLimit) {
|
||||||
|
AssetService service;
|
||||||
|
service.init();
|
||||||
|
|
||||||
|
service.setLimit(1024 * 1024);
|
||||||
|
|
||||||
|
service.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 加载器注册测试
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST(AssetService, RegisterLoader) {
|
||||||
|
AssetService service;
|
||||||
|
service.init();
|
||||||
|
|
||||||
|
service.registerLoader<TestResource>(std::make_unique<TestResourceLoader>());
|
||||||
|
|
||||||
|
service.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 资源加载测试
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST(AssetService, LoadNonExistent) {
|
||||||
|
setupTestDir();
|
||||||
|
|
||||||
|
AssetService service;
|
||||||
|
service.init();
|
||||||
|
service.setRoot(testDir);
|
||||||
|
service.registerLoader<TestResource>(std::make_unique<TestResourceLoader>());
|
||||||
|
|
||||||
|
auto handle = service.load<TestResource>("nonexistent.test");
|
||||||
|
|
||||||
|
TEST_ASSERT_FALSE(handle.valid());
|
||||||
|
|
||||||
|
service.shutdown();
|
||||||
|
cleanupTestDir();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(AssetService, LoadFromMemory) {
|
||||||
|
AssetService service;
|
||||||
|
service.init();
|
||||||
|
service.registerLoader<TestResource>(std::make_unique<TestResourceLoader>());
|
||||||
|
|
||||||
|
std::vector<u8> testData = createTestData(256);
|
||||||
|
|
||||||
|
service.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(AssetService, IsLoaded) {
|
||||||
|
setupTestDir();
|
||||||
|
|
||||||
|
std::string filePath = getTestPath("resource.test");
|
||||||
|
writeTestFile(filePath, createTestData(256));
|
||||||
|
|
||||||
|
AssetService service;
|
||||||
|
service.init();
|
||||||
|
service.setRoot(testDir);
|
||||||
|
service.registerLoader<TestResource>(std::make_unique<TestResourceLoader>());
|
||||||
|
|
||||||
|
TEST_ASSERT_FALSE(service.isLoaded("resource.test"));
|
||||||
|
|
||||||
|
auto handle = service.load<TestResource>("resource.test");
|
||||||
|
|
||||||
|
TEST_ASSERT_TRUE(service.isLoaded("resource.test"));
|
||||||
|
|
||||||
|
service.shutdown();
|
||||||
|
cleanupTestDir();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 缓存测试
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST(AssetService, CacheHit) {
|
||||||
|
setupTestDir();
|
||||||
|
|
||||||
|
std::string filePath = getTestPath("cached.test");
|
||||||
|
writeTestFile(filePath, createTestData(256));
|
||||||
|
|
||||||
|
AssetService service;
|
||||||
|
service.init();
|
||||||
|
service.setRoot(testDir);
|
||||||
|
service.registerLoader<TestResource>(std::make_unique<TestResourceLoader>());
|
||||||
|
|
||||||
|
auto handle1 = service.load<TestResource>("cached.test");
|
||||||
|
auto handle2 = service.get<TestResource>("cached.test");
|
||||||
|
|
||||||
|
TEST_ASSERT_TRUE(handle1.valid());
|
||||||
|
TEST_ASSERT_TRUE(handle2.valid());
|
||||||
|
TEST_ASSERT_EQ(handle1.get().get(), handle2.get().get());
|
||||||
|
|
||||||
|
auto stats = service.stats();
|
||||||
|
TEST_ASSERT_GE(stats.hits, 1u);
|
||||||
|
|
||||||
|
service.shutdown();
|
||||||
|
cleanupTestDir();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(AssetService, CacheMiss) {
|
||||||
|
AssetService service;
|
||||||
|
service.init();
|
||||||
|
|
||||||
|
auto handle = service.get<TestResource>("not_in_cache.test");
|
||||||
|
|
||||||
|
TEST_ASSERT_FALSE(handle.valid());
|
||||||
|
|
||||||
|
auto stats = service.stats();
|
||||||
|
TEST_ASSERT_GE(stats.misses, 1u);
|
||||||
|
|
||||||
|
service.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(AssetService, Purge) {
|
||||||
|
setupTestDir();
|
||||||
|
|
||||||
|
std::string filePath = getTestPath("purge.test");
|
||||||
|
writeTestFile(filePath, createTestData(256));
|
||||||
|
|
||||||
|
AssetService service;
|
||||||
|
service.init();
|
||||||
|
service.setRoot(testDir);
|
||||||
|
service.registerLoader<TestResource>(std::make_unique<TestResourceLoader>());
|
||||||
|
|
||||||
|
{
|
||||||
|
auto handle = service.load<TestResource>("purge.test");
|
||||||
|
}
|
||||||
|
|
||||||
|
service.purge();
|
||||||
|
|
||||||
|
TEST_ASSERT_EQ(0u, service.size());
|
||||||
|
|
||||||
|
service.shutdown();
|
||||||
|
cleanupTestDir();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(AssetService, Clear) {
|
||||||
|
setupTestDir();
|
||||||
|
|
||||||
|
std::string filePath = getTestPath("clear.test");
|
||||||
|
writeTestFile(filePath, createTestData(256));
|
||||||
|
|
||||||
|
AssetService service;
|
||||||
|
service.init();
|
||||||
|
service.setRoot(testDir);
|
||||||
|
service.registerLoader<TestResource>(std::make_unique<TestResourceLoader>());
|
||||||
|
|
||||||
|
auto handle = service.load<TestResource>("clear.test");
|
||||||
|
|
||||||
|
TEST_ASSERT_GT(service.size(), 0u);
|
||||||
|
|
||||||
|
service.clear();
|
||||||
|
|
||||||
|
TEST_ASSERT_EQ(0u, service.size());
|
||||||
|
|
||||||
|
service.shutdown();
|
||||||
|
cleanupTestDir();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 卸载测试
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST(AssetService, Unload) {
|
||||||
|
setupTestDir();
|
||||||
|
|
||||||
|
std::string filePath = getTestPath("unload.test");
|
||||||
|
writeTestFile(filePath, createTestData(256));
|
||||||
|
|
||||||
|
AssetService service;
|
||||||
|
service.init();
|
||||||
|
service.setRoot(testDir);
|
||||||
|
service.registerLoader<TestResource>(std::make_unique<TestResourceLoader>());
|
||||||
|
|
||||||
|
auto handle = service.load<TestResource>("unload.test");
|
||||||
|
|
||||||
|
TEST_ASSERT_TRUE(service.isLoaded("unload.test"));
|
||||||
|
|
||||||
|
service.unload("unload.test");
|
||||||
|
|
||||||
|
TEST_ASSERT_FALSE(service.isLoaded("unload.test"));
|
||||||
|
|
||||||
|
service.shutdown();
|
||||||
|
cleanupTestDir();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 资源包挂载测试
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST(AssetService, MountPack) {
|
||||||
|
setupTestDir();
|
||||||
|
|
||||||
|
AssetPackBuilder builder;
|
||||||
|
builder.add("packed.test", createTestData(256));
|
||||||
|
builder.build(getTestPath("service.pack"));
|
||||||
|
|
||||||
|
AssetService service;
|
||||||
|
service.init();
|
||||||
|
service.registerLoader<TestResource>(std::make_unique<TestResourceLoader>());
|
||||||
|
|
||||||
|
bool mounted = service.mount(getTestPath("service.pack"));
|
||||||
|
|
||||||
|
TEST_ASSERT_TRUE(mounted);
|
||||||
|
|
||||||
|
service.shutdown();
|
||||||
|
cleanupTestDir();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(AssetService, UnmountPack) {
|
||||||
|
setupTestDir();
|
||||||
|
|
||||||
|
AssetPackBuilder builder;
|
||||||
|
builder.add("packed.test", createTestData(256));
|
||||||
|
builder.build(getTestPath("unmount.pack"));
|
||||||
|
|
||||||
|
AssetService service;
|
||||||
|
service.init();
|
||||||
|
service.registerLoader<TestResource>(std::make_unique<TestResourceLoader>());
|
||||||
|
|
||||||
|
service.mount(getTestPath("unmount.pack"));
|
||||||
|
service.unmount(getTestPath("unmount.pack"));
|
||||||
|
|
||||||
|
service.shutdown();
|
||||||
|
cleanupTestDir();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 统计测试
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST(AssetService, Stats) {
|
||||||
|
AssetService service;
|
||||||
|
service.init();
|
||||||
|
|
||||||
|
auto stats = service.stats();
|
||||||
|
|
||||||
|
TEST_ASSERT_EQ(0u, stats.hits);
|
||||||
|
TEST_ASSERT_EQ(0u, stats.misses);
|
||||||
|
TEST_ASSERT_EQ(0u, stats.count);
|
||||||
|
|
||||||
|
service.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 数据处理管道测试
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST(AssetService, SetPipe) {
|
||||||
|
AssetService service;
|
||||||
|
service.init();
|
||||||
|
|
||||||
|
DataPipe pipe;
|
||||||
|
pipe.encrypt("key", Decryptor::Type::XOR);
|
||||||
|
|
||||||
|
service.setPipe(std::move(pipe));
|
||||||
|
|
||||||
|
service.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 异步加载测试
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST(AssetService, AsyncLoad) {
|
||||||
|
setupTestDir();
|
||||||
|
|
||||||
|
std::string filePath = getTestPath("async.test");
|
||||||
|
writeTestFile(filePath, createTestData(512));
|
||||||
|
|
||||||
|
AssetService service;
|
||||||
|
service.init();
|
||||||
|
service.setRoot(testDir);
|
||||||
|
service.registerLoader<TestResource>(std::make_unique<TestResourceLoader>());
|
||||||
|
|
||||||
|
std::atomic<bool> callbackCalled{false};
|
||||||
|
AssetHandle<TestResource> resultHandle;
|
||||||
|
|
||||||
|
service.loadAsync<TestResource>("async.test",
|
||||||
|
[&](AssetHandle<TestResource> handle) {
|
||||||
|
resultHandle = handle;
|
||||||
|
callbackCalled = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
for (int i = 0; i < 100 && !callbackCalled; ++i) {
|
||||||
|
service.process();
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||||
|
}
|
||||||
|
|
||||||
|
service.process();
|
||||||
|
|
||||||
|
TEST_ASSERT_TRUE(callbackCalled);
|
||||||
|
TEST_ASSERT_TRUE(resultHandle.valid());
|
||||||
|
|
||||||
|
service.shutdown();
|
||||||
|
cleanupTestDir();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(AssetService, Preload) {
|
||||||
|
setupTestDir();
|
||||||
|
|
||||||
|
std::string filePath = getTestPath("preload.test");
|
||||||
|
writeTestFile(filePath, createTestData(256));
|
||||||
|
|
||||||
|
AssetService service;
|
||||||
|
service.init();
|
||||||
|
service.setRoot(testDir);
|
||||||
|
service.registerLoader<TestResource>(std::make_unique<TestResourceLoader>());
|
||||||
|
|
||||||
|
service.preload<TestResource>("preload.test");
|
||||||
|
|
||||||
|
for (int i = 0; i < 50; ++i) {
|
||||||
|
service.process();
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||||
|
}
|
||||||
|
|
||||||
|
service.shutdown();
|
||||||
|
cleanupTestDir();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 多资源测试
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST(AssetService, LoadMultiple) {
|
||||||
|
setupTestDir();
|
||||||
|
|
||||||
|
for (int i = 0; i < 5; ++i) {
|
||||||
|
std::string filePath = getTestPath("multi" + std::to_string(i) + ".test");
|
||||||
|
writeTestFile(filePath, createTestData(100 * (i + 1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
AssetService service;
|
||||||
|
service.init();
|
||||||
|
service.setRoot(testDir);
|
||||||
|
service.registerLoader<TestResource>(std::make_unique<TestResourceLoader>());
|
||||||
|
|
||||||
|
for (int i = 0; i < 5; ++i) {
|
||||||
|
auto handle = service.load<TestResource>("multi" + std::to_string(i) + ".test");
|
||||||
|
TEST_ASSERT_TRUE(handle.valid());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_ASSERT_GT(service.size(), 0u);
|
||||||
|
|
||||||
|
service.shutdown();
|
||||||
|
cleanupTestDir();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,260 @@
|
||||||
|
/**
|
||||||
|
* @file test_data_processor.cpp
|
||||||
|
* @brief DataProcessor 单元测试
|
||||||
|
*
|
||||||
|
* 测试数据处理管道、加密/解密、压缩/解压功能。
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "test_framework.h"
|
||||||
|
#include <extra2d/asset/data_processor.h>
|
||||||
|
|
||||||
|
using namespace extra2d;
|
||||||
|
using namespace extra2d::test;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// XOR 加密/解密测试
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST(DataProcessor, XOR_EncryptDecrypt) {
|
||||||
|
std::vector<u8> original = createTestData(1024);
|
||||||
|
std::string key = "test-key-12345";
|
||||||
|
|
||||||
|
Encryptor encryptor(key, Decryptor::Type::XOR);
|
||||||
|
Decryptor decryptor(key, Decryptor::Type::XOR);
|
||||||
|
|
||||||
|
std::vector<u8> encrypted = encryptor.process(original);
|
||||||
|
std::vector<u8> decrypted = decryptor.process(encrypted);
|
||||||
|
|
||||||
|
TEST_ASSERT_TRUE(dataEquals(original, decrypted));
|
||||||
|
TEST_ASSERT_FALSE(dataEquals(original, encrypted));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DataProcessor, XOR_SameKeyProducesSameResult) {
|
||||||
|
std::vector<u8> data = createTestData(512);
|
||||||
|
std::string key = "secret";
|
||||||
|
|
||||||
|
Encryptor enc1(key, Decryptor::Type::XOR);
|
||||||
|
Encryptor enc2(key, Decryptor::Type::XOR);
|
||||||
|
|
||||||
|
std::vector<u8> result1 = enc1.process(data);
|
||||||
|
std::vector<u8> result2 = enc2.process(data);
|
||||||
|
|
||||||
|
TEST_ASSERT_TRUE(dataEquals(result1, result2));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DataProcessor, XOR_DifferentKeysProduceDifferentResults) {
|
||||||
|
std::vector<u8> data = createTestData(512);
|
||||||
|
|
||||||
|
Encryptor enc1("key1", Decryptor::Type::XOR);
|
||||||
|
Encryptor enc2("key2", Decryptor::Type::XOR);
|
||||||
|
|
||||||
|
std::vector<u8> result1 = enc1.process(data);
|
||||||
|
std::vector<u8> result2 = enc2.process(data);
|
||||||
|
|
||||||
|
TEST_ASSERT_FALSE(dataEquals(result1, result2));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DataProcessor, XOR_EmptyKey) {
|
||||||
|
std::vector<u8> data = createTestData(256);
|
||||||
|
|
||||||
|
Encryptor encryptor("", Decryptor::Type::XOR);
|
||||||
|
Decryptor decryptor("", Decryptor::Type::XOR);
|
||||||
|
|
||||||
|
std::vector<u8> encrypted = encryptor.process(data);
|
||||||
|
std::vector<u8> decrypted = decryptor.process(encrypted);
|
||||||
|
|
||||||
|
TEST_ASSERT_TRUE(dataEquals(data, decrypted));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DataProcessor, XOR_EmptyData) {
|
||||||
|
std::vector<u8> empty;
|
||||||
|
std::string key = "test";
|
||||||
|
|
||||||
|
Encryptor encryptor(key, Decryptor::Type::XOR);
|
||||||
|
Decryptor decryptor(key, Decryptor::Type::XOR);
|
||||||
|
|
||||||
|
std::vector<u8> encrypted = encryptor.process(empty);
|
||||||
|
std::vector<u8> decrypted = decryptor.process(encrypted);
|
||||||
|
|
||||||
|
TEST_ASSERT_TRUE(decrypted.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// DataPipe 测试
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST(DataProcessor, DataPipe_Empty) {
|
||||||
|
DataPipe pipe;
|
||||||
|
|
||||||
|
TEST_ASSERT_TRUE(pipe.empty());
|
||||||
|
TEST_ASSERT_EQ(0u, pipe.size());
|
||||||
|
|
||||||
|
std::vector<u8> data = createTestData(100);
|
||||||
|
std::vector<u8> result = pipe.process(data);
|
||||||
|
|
||||||
|
TEST_ASSERT_TRUE(dataEquals(data, result));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DataProcessor, DataPipe_SingleProcessor) {
|
||||||
|
DataPipe pipe;
|
||||||
|
pipe.encrypt("key", Decryptor::Type::XOR);
|
||||||
|
|
||||||
|
TEST_ASSERT_FALSE(pipe.empty());
|
||||||
|
TEST_ASSERT_EQ(1u, pipe.size());
|
||||||
|
|
||||||
|
std::vector<u8> original = createTestData(256);
|
||||||
|
std::vector<u8> processed = pipe.process(original);
|
||||||
|
|
||||||
|
TEST_ASSERT_FALSE(dataEquals(original, processed));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DataProcessor, DataPipe_MultipleProcessors) {
|
||||||
|
DataPipe pipe;
|
||||||
|
pipe.encrypt("key", Decryptor::Type::XOR);
|
||||||
|
pipe.encrypt("key2", Decryptor::Type::XOR);
|
||||||
|
pipe.encrypt("key", Decryptor::Type::XOR);
|
||||||
|
pipe.encrypt("key2", Decryptor::Type::XOR);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQ(4u, pipe.size());
|
||||||
|
|
||||||
|
std::vector<u8> original = createTestData(512);
|
||||||
|
std::vector<u8> processed = pipe.process(original);
|
||||||
|
|
||||||
|
TEST_ASSERT_TRUE(dataEquals(original, processed));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DataProcessor, DataPipe_EncryptDecrypt) {
|
||||||
|
DataPipe encryptPipe;
|
||||||
|
encryptPipe.encrypt("secret-key", Decryptor::Type::XOR);
|
||||||
|
|
||||||
|
DataPipe decryptPipe;
|
||||||
|
decryptPipe.decrypt("secret-key", Decryptor::Type::XOR);
|
||||||
|
|
||||||
|
std::vector<u8> original = createTestData(1024);
|
||||||
|
std::vector<u8> encrypted = encryptPipe.process(original);
|
||||||
|
std::vector<u8> decrypted = decryptPipe.process(encrypted);
|
||||||
|
|
||||||
|
TEST_ASSERT_TRUE(dataEquals(original, decrypted));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DataProcessor, DataPipe_Clear) {
|
||||||
|
DataPipe pipe;
|
||||||
|
pipe.encrypt("key", Decryptor::Type::XOR);
|
||||||
|
pipe.encrypt("key2", Decryptor::Type::XOR);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQ(2u, pipe.size());
|
||||||
|
|
||||||
|
pipe.clear();
|
||||||
|
|
||||||
|
TEST_ASSERT_TRUE(pipe.empty());
|
||||||
|
TEST_ASSERT_EQ(0u, pipe.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DataProcessor, DataPipe_MoveSemantics) {
|
||||||
|
DataPipe pipe1;
|
||||||
|
pipe1.encrypt("key", Decryptor::Type::XOR);
|
||||||
|
|
||||||
|
DataPipe pipe2 = std::move(pipe1);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQ(1u, pipe2.size());
|
||||||
|
|
||||||
|
std::vector<u8> data = createTestData(128);
|
||||||
|
std::vector<u8> result = pipe2.process(data);
|
||||||
|
|
||||||
|
TEST_ASSERT_FALSE(dataEquals(data, result));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 校验和测试
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST(DataProcessor, Checksum_Consistency) {
|
||||||
|
std::vector<u8> data = createTestData(1024);
|
||||||
|
|
||||||
|
std::vector<u8> checksum1 = computeChecksum(data);
|
||||||
|
std::vector<u8> checksum2 = computeChecksum(data);
|
||||||
|
|
||||||
|
TEST_ASSERT_TRUE(dataEquals(checksum1, checksum2));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DataProcessor, Checksum_DifferentData) {
|
||||||
|
std::vector<u8> data1 = createTestData(1024, 42);
|
||||||
|
std::vector<u8> data2 = createTestData(1024, 43);
|
||||||
|
|
||||||
|
std::vector<u8> checksum1 = computeChecksum(data1);
|
||||||
|
std::vector<u8> checksum2 = computeChecksum(data2);
|
||||||
|
|
||||||
|
TEST_ASSERT_FALSE(dataEquals(checksum1, checksum2));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DataProcessor, Checksum_Verify) {
|
||||||
|
std::vector<u8> data = createTestData(2048);
|
||||||
|
std::vector<u8> checksum = computeChecksum(data);
|
||||||
|
|
||||||
|
TEST_ASSERT_TRUE(verifyChecksum(data, checksum));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DataProcessor, Checksum_VerifyTampered) {
|
||||||
|
std::vector<u8> data = createTestData(2048);
|
||||||
|
std::vector<u8> checksum = computeChecksum(data);
|
||||||
|
|
||||||
|
data[100] ^= 0xFF;
|
||||||
|
|
||||||
|
TEST_ASSERT_FALSE(verifyChecksum(data, checksum));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DataProcessor, Checksum_Size) {
|
||||||
|
std::vector<u8> data = createTestData(100);
|
||||||
|
std::vector<u8> checksum = computeChecksum(data);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQ(32u, checksum.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 处理器链测试
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST(DataProcessor, Chain_EncryptDecrypt) {
|
||||||
|
std::vector<u8> original = createTestData(512);
|
||||||
|
std::string key = "chain-test-key";
|
||||||
|
|
||||||
|
Encryptor encryptor(key, Decryptor::Type::XOR);
|
||||||
|
Decryptor decryptor(key, Decryptor::Type::XOR);
|
||||||
|
|
||||||
|
encryptor.setNext(std::make_unique<Decryptor>(key, Decryptor::Type::XOR));
|
||||||
|
|
||||||
|
std::vector<u8> result = encryptor.process(original);
|
||||||
|
|
||||||
|
TEST_ASSERT_TRUE(dataEquals(original, result));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 边界条件测试
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST(DataProcessor, Boundary_SingleByte) {
|
||||||
|
std::vector<u8> data = {0x42};
|
||||||
|
std::string key = "key";
|
||||||
|
|
||||||
|
Encryptor encryptor(key, Decryptor::Type::XOR);
|
||||||
|
Decryptor decryptor(key, Decryptor::Type::XOR);
|
||||||
|
|
||||||
|
std::vector<u8> encrypted = encryptor.process(data);
|
||||||
|
std::vector<u8> decrypted = decryptor.process(encrypted);
|
||||||
|
|
||||||
|
TEST_ASSERT_TRUE(dataEquals(data, decrypted));
|
||||||
|
TEST_ASSERT_EQ(1u, encrypted.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DataProcessor, Boundary_LargeData) {
|
||||||
|
std::vector<u8> data = createTestData(1024 * 1024);
|
||||||
|
std::string key = "large-data-key";
|
||||||
|
|
||||||
|
Encryptor encryptor(key, Decryptor::Type::XOR);
|
||||||
|
Decryptor decryptor(key, Decryptor::Type::XOR);
|
||||||
|
|
||||||
|
std::vector<u8> encrypted = encryptor.process(data);
|
||||||
|
std::vector<u8> decrypted = decryptor.process(encrypted);
|
||||||
|
|
||||||
|
TEST_ASSERT_TRUE(dataEquals(data, decrypted));
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
/**
|
||||||
|
* @file test_framework.cpp
|
||||||
|
* @brief 测试框架实现
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "test_framework.h"
|
||||||
|
|
||||||
|
namespace extra2d {
|
||||||
|
namespace test {
|
||||||
|
|
||||||
|
static TestStats g_stats;
|
||||||
|
|
||||||
|
std::vector<TestCase>& getTestCases() {
|
||||||
|
static std::vector<TestCase> cases;
|
||||||
|
return cases;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestRegistrar::TestRegistrar(const std::string& suite, const std::string& name, TestFunc func) {
|
||||||
|
getTestCases().push_back({name, suite, func});
|
||||||
|
}
|
||||||
|
|
||||||
|
int runAllTests() {
|
||||||
|
auto& cases = getTestCases();
|
||||||
|
std::cout << "========================================\n";
|
||||||
|
std::cout << "Running " << cases.size() << " tests...\n";
|
||||||
|
std::cout << "========================================\n\n";
|
||||||
|
|
||||||
|
std::string currentSuite;
|
||||||
|
|
||||||
|
for (const auto& tc : cases) {
|
||||||
|
if (tc.suite != currentSuite) {
|
||||||
|
currentSuite = tc.suite;
|
||||||
|
std::cout << "\n[" << currentSuite << "]\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
auto start = std::chrono::high_resolution_clock::now();
|
||||||
|
|
||||||
|
try {
|
||||||
|
tc.func();
|
||||||
|
auto end = std::chrono::high_resolution_clock::now();
|
||||||
|
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
|
||||||
|
|
||||||
|
std::cout << " [PASS] " << tc.name << " (" << duration.count() << "ms)\n";
|
||||||
|
g_stats.passed++;
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
auto end = std::chrono::high_resolution_clock::now();
|
||||||
|
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
|
||||||
|
|
||||||
|
std::cout << " [FAIL] " << tc.name << " (" << duration.count() << "ms)\n";
|
||||||
|
std::cout << " Error: " << e.what() << "\n";
|
||||||
|
g_stats.failed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "\n========================================\n";
|
||||||
|
std::cout << "Results: " << g_stats.passed << " passed, "
|
||||||
|
<< g_stats.failed << " failed, "
|
||||||
|
<< g_stats.skipped << " skipped\n";
|
||||||
|
std::cout << "========================================\n";
|
||||||
|
|
||||||
|
return g_stats.failed > 0 ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace test
|
||||||
|
} // namespace extra2d
|
||||||
|
|
@ -0,0 +1,140 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file test_framework.h
|
||||||
|
* @brief 轻量级测试框架
|
||||||
|
*
|
||||||
|
* 提供简单的测试断言和测试注册机制。
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <extra2d/core/types.h>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <functional>
|
||||||
|
#include <iostream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace extra2d {
|
||||||
|
namespace test {
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 测试结果统计
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
struct TestStats {
|
||||||
|
int passed = 0;
|
||||||
|
int failed = 0;
|
||||||
|
int skipped = 0;
|
||||||
|
|
||||||
|
int total() const { return passed + failed + skipped; }
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 测试断言
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#define TEST_ASSERT(condition) \
|
||||||
|
do { \
|
||||||
|
if (!(condition)) { \
|
||||||
|
std::ostringstream _ss; \
|
||||||
|
_ss << "Assertion failed: " << #condition \
|
||||||
|
<< " at " << __FILE__ << ":" << __LINE__; \
|
||||||
|
throw std::runtime_error(_ss.str()); \
|
||||||
|
} \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
#define TEST_ASSERT_EQ(expected, actual) \
|
||||||
|
do { \
|
||||||
|
if (!((expected) == (actual))) { \
|
||||||
|
std::ostringstream _ss; \
|
||||||
|
_ss << "Assertion failed: expected " << (expected) \
|
||||||
|
<< " but got " << (actual) \
|
||||||
|
<< " at " << __FILE__ << ":" << __LINE__; \
|
||||||
|
throw std::runtime_error(_ss.str()); \
|
||||||
|
} \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
#define TEST_ASSERT_NE(val1, val2) \
|
||||||
|
do { \
|
||||||
|
if ((val1) == (val2)) { \
|
||||||
|
std::ostringstream _ss; \
|
||||||
|
_ss << "Assertion failed: " << (val1) \
|
||||||
|
<< " should not equal " << (val2) \
|
||||||
|
<< " at " << __FILE__ << ":" << __LINE__; \
|
||||||
|
throw std::runtime_error(_ss.str()); \
|
||||||
|
} \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
#define TEST_ASSERT_TRUE(condition) TEST_ASSERT(condition)
|
||||||
|
#define TEST_ASSERT_FALSE(condition) TEST_ASSERT(!(condition))
|
||||||
|
#define TEST_ASSERT_NULL(ptr) TEST_ASSERT((ptr) == nullptr)
|
||||||
|
#define TEST_ASSERT_NOT_NULL(ptr) TEST_ASSERT((ptr) != nullptr)
|
||||||
|
#define TEST_ASSERT_GT(val1, val2) TEST_ASSERT((val1) > (val2))
|
||||||
|
#define TEST_ASSERT_GE(val1, val2) TEST_ASSERT((val1) >= (val2))
|
||||||
|
#define TEST_ASSERT_LT(val1, val2) TEST_ASSERT((val1) < (val2))
|
||||||
|
#define TEST_ASSERT_LE(val1, val2) TEST_ASSERT((val1) <= (val2))
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 测试用例
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
using TestFunc = std::function<void()>;
|
||||||
|
|
||||||
|
struct TestCase {
|
||||||
|
std::string name;
|
||||||
|
std::string suite;
|
||||||
|
TestFunc func;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<TestCase>& getTestCases();
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 测试注册器
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class TestRegistrar {
|
||||||
|
public:
|
||||||
|
TestRegistrar(const std::string& suite, const std::string& name, TestFunc func);
|
||||||
|
};
|
||||||
|
|
||||||
|
#define TEST(suite, name) \
|
||||||
|
static void test_##suite##_##name(); \
|
||||||
|
static extra2d::test::TestRegistrar registrar_##suite##_##name(#suite, #name, test_##suite##_##name); \
|
||||||
|
static void test_##suite##_##name()
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 测试运行器
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
int runAllTests();
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 测试工具函数
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 创建测试数据
|
||||||
|
* @param size 数据大小
|
||||||
|
* @param seed 随机种子
|
||||||
|
* @return 测试数据
|
||||||
|
*/
|
||||||
|
inline std::vector<u8> createTestData(size_t size, u32 seed = 42) {
|
||||||
|
std::vector<u8> data(size);
|
||||||
|
for (size_t i = 0; i < size; ++i) {
|
||||||
|
data[i] = static_cast<u8>((seed + i) % 256);
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 比较两个数据是否相等
|
||||||
|
*/
|
||||||
|
inline bool dataEquals(const std::vector<u8>& a, const std::vector<u8>& b) {
|
||||||
|
if (a.size() != b.size()) return false;
|
||||||
|
return std::equal(a.begin(), a.end(), b.begin());
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace test
|
||||||
|
} // namespace extra2d
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
/**
|
||||||
|
* @file test_main.cpp
|
||||||
|
* @brief 测试主入口
|
||||||
|
*
|
||||||
|
* 运行所有资源服务系统的单元测试。
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "test_framework.h"
|
||||||
|
|
||||||
|
int main(int argc, char* argv[]) {
|
||||||
|
(void)argc;
|
||||||
|
(void)argv;
|
||||||
|
|
||||||
|
return extra2d::test::runAllTests();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,460 @@
|
||||||
|
/**
|
||||||
|
* @file asset_packer.cpp
|
||||||
|
* @brief 资源打包工具
|
||||||
|
*
|
||||||
|
* 命令行工具,用于创建、查看和提取资源包。
|
||||||
|
*
|
||||||
|
* 用法:
|
||||||
|
* asset_packer create <output.pack> [options] <input files/dirs...>
|
||||||
|
* asset_packer list <pack file>
|
||||||
|
* asset_packer extract <pack file> <output dir>
|
||||||
|
*
|
||||||
|
* 选项:
|
||||||
|
* -c, --compression <algo> 压缩算法 (none, zstd, lz4, zlib),默认 zstd
|
||||||
|
* -l, --level <level> 压缩级别 (1-22),默认 3
|
||||||
|
* -e, --encrypt <key> 加密密钥
|
||||||
|
* -t, --encrypt-type <type> 加密类型 (xor, aes),默认 xor
|
||||||
|
* -v, --verbose 详细输出
|
||||||
|
* -h, --help 显示帮助
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <extra2d/asset/asset_pack.h>
|
||||||
|
#include <extra2d/asset/data_processor.h>
|
||||||
|
#include <extra2d/core/types.h>
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstring>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iostream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 命令行参数解析
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
struct Options {
|
||||||
|
std::string command;
|
||||||
|
std::string outputFile;
|
||||||
|
std::string packFile;
|
||||||
|
std::string outputDir;
|
||||||
|
std::vector<std::string> inputs;
|
||||||
|
|
||||||
|
extra2d::Compression compression = extra2d::Compression::Zstd;
|
||||||
|
int compressionLevel = 3;
|
||||||
|
std::string encryptKey;
|
||||||
|
extra2d::Decryptor::Type encryptType = extra2d::Decryptor::Type::XOR;
|
||||||
|
|
||||||
|
bool verbose = false;
|
||||||
|
bool showHelp = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 打印帮助信息
|
||||||
|
*/
|
||||||
|
void printHelp(const char* programName) {
|
||||||
|
std::cout << "Extra2D 资源打包工具 v1.0.0\n\n";
|
||||||
|
std::cout << "用法:\n";
|
||||||
|
std::cout << " " << programName << " create <output.pack> [options] <inputs...>\n";
|
||||||
|
std::cout << " " << programName << " list <pack file>\n";
|
||||||
|
std::cout << " " << programName << " extract <pack file> <output dir>\n\n";
|
||||||
|
std::cout << "命令:\n";
|
||||||
|
std::cout << " create 创建资源包\n";
|
||||||
|
std::cout << " list 列出资源包内容\n";
|
||||||
|
std::cout << " extract 提取资源包内容\n\n";
|
||||||
|
std::cout << "选项:\n";
|
||||||
|
std::cout << " -c, --compression <algo> 压缩算法 (none, zstd, lz4, zlib),默认 zstd\n";
|
||||||
|
std::cout << " -l, --level <level> 压缩级别 (1-22),默认 3\n";
|
||||||
|
std::cout << " -e, --encrypt <key> 加密密钥\n";
|
||||||
|
std::cout << " -t, --encrypt-type <type> 加密类型 (xor, aes),默认 xor\n";
|
||||||
|
std::cout << " -v, --verbose 详细输出\n";
|
||||||
|
std::cout << " -h, --help 显示帮助\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 解析压缩算法名称
|
||||||
|
*/
|
||||||
|
extra2d::Compression parseCompression(const std::string& name) {
|
||||||
|
if (name == "none" || name == "None" || name == "NONE") {
|
||||||
|
return extra2d::Compression::None;
|
||||||
|
} else if (name == "zstd" || name == "Zstd" || name == "ZSTD") {
|
||||||
|
return extra2d::Compression::Zstd;
|
||||||
|
} else if (name == "lz4" || name == "LZ4") {
|
||||||
|
return extra2d::Compression::LZ4;
|
||||||
|
} else if (name == "zlib" || name == "Zlib" || name == "ZLIB") {
|
||||||
|
return extra2d::Compression::Zlib;
|
||||||
|
}
|
||||||
|
return extra2d::Compression::Zstd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 解析加密类型
|
||||||
|
*/
|
||||||
|
extra2d::Decryptor::Type parseEncryptType(const std::string& name) {
|
||||||
|
if (name == "xor" || name == "XOR") {
|
||||||
|
return extra2d::Decryptor::Type::XOR;
|
||||||
|
} else if (name == "aes" || name == "AES" || name == "aes256") {
|
||||||
|
return extra2d::Decryptor::Type::AES256;
|
||||||
|
}
|
||||||
|
return extra2d::Decryptor::Type::XOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 解析命令行参数
|
||||||
|
*/
|
||||||
|
bool parseArgs(int argc, char* argv[], Options& opts) {
|
||||||
|
if (argc < 2) {
|
||||||
|
opts.showHelp = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.command = argv[1];
|
||||||
|
|
||||||
|
if (opts.command == "-h" || opts.command == "--help") {
|
||||||
|
opts.showHelp = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 2; i < argc; ++i) {
|
||||||
|
std::string arg = argv[i];
|
||||||
|
|
||||||
|
if (arg == "-h" || arg == "--help") {
|
||||||
|
opts.showHelp = true;
|
||||||
|
return true;
|
||||||
|
} else if (arg == "-v" || arg == "--verbose") {
|
||||||
|
opts.verbose = true;
|
||||||
|
} else if (arg == "-c" || arg == "--compression") {
|
||||||
|
if (i + 1 < argc) {
|
||||||
|
opts.compression = parseCompression(argv[++i]);
|
||||||
|
}
|
||||||
|
} else if (arg == "-l" || arg == "--level") {
|
||||||
|
if (i + 1 < argc) {
|
||||||
|
opts.compressionLevel = std::stoi(argv[++i]);
|
||||||
|
}
|
||||||
|
} else if (arg == "-e" || arg == "--encrypt") {
|
||||||
|
if (i + 1 < argc) {
|
||||||
|
opts.encryptKey = argv[++i];
|
||||||
|
}
|
||||||
|
} else if (arg == "-t" || arg == "--encrypt-type") {
|
||||||
|
if (i + 1 < argc) {
|
||||||
|
opts.encryptType = parseEncryptType(argv[++i]);
|
||||||
|
}
|
||||||
|
} else if (arg[0] != '-') {
|
||||||
|
if (opts.command == "create") {
|
||||||
|
if (opts.outputFile.empty()) {
|
||||||
|
opts.outputFile = arg;
|
||||||
|
} else {
|
||||||
|
opts.inputs.push_back(arg);
|
||||||
|
}
|
||||||
|
} else if (opts.command == "list") {
|
||||||
|
if (opts.packFile.empty()) {
|
||||||
|
opts.packFile = arg;
|
||||||
|
}
|
||||||
|
} else if (opts.command == "extract") {
|
||||||
|
if (opts.packFile.empty()) {
|
||||||
|
opts.packFile = arg;
|
||||||
|
} else if (opts.outputDir.empty()) {
|
||||||
|
opts.outputDir = arg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 创建资源包
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 格式化文件大小
|
||||||
|
*/
|
||||||
|
std::string formatSize(size_t bytes) {
|
||||||
|
const char* units[] = {"B", "KB", "MB", "GB"};
|
||||||
|
int unitIndex = 0;
|
||||||
|
double size = static_cast<double>(bytes);
|
||||||
|
|
||||||
|
while (size >= 1024.0 && unitIndex < 3) {
|
||||||
|
size /= 1024.0;
|
||||||
|
++unitIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ostringstream ss;
|
||||||
|
ss.precision(2);
|
||||||
|
ss << size << " " << units[unitIndex];
|
||||||
|
return ss.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 创建资源包
|
||||||
|
*/
|
||||||
|
int createPack(const Options& opts) {
|
||||||
|
if (opts.outputFile.empty()) {
|
||||||
|
std::cerr << "错误: 未指定输出文件\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.inputs.empty()) {
|
||||||
|
std::cerr << "错误: 未指定输入文件或目录\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto startTime = std::chrono::high_resolution_clock::now();
|
||||||
|
|
||||||
|
extra2d::AssetPackBuilder builder(opts.compression, opts.compressionLevel);
|
||||||
|
|
||||||
|
if (!opts.encryptKey.empty()) {
|
||||||
|
builder.setEncryption(opts.encryptKey, opts.encryptType);
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t totalFiles = 0;
|
||||||
|
size_t totalSize = 0;
|
||||||
|
|
||||||
|
for (const auto& input : opts.inputs) {
|
||||||
|
fs::path inputPath(input);
|
||||||
|
|
||||||
|
if (!fs::exists(inputPath)) {
|
||||||
|
std::cerr << "警告: 跳过不存在的路径: " << input << "\n";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs::is_directory(inputPath)) {
|
||||||
|
if (opts.verbose) {
|
||||||
|
std::cout << "扫描目录: " << input << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& entry : fs::recursive_directory_iterator(inputPath)) {
|
||||||
|
if (entry.is_regular_file()) {
|
||||||
|
fs::path relativePath = fs::relative(entry.path(), inputPath);
|
||||||
|
std::string packPath = relativePath.generic_string();
|
||||||
|
|
||||||
|
if (opts.verbose) {
|
||||||
|
std::cout << " 添加: " << packPath << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (builder.addFile(entry.path().string(), packPath)) {
|
||||||
|
++totalFiles;
|
||||||
|
totalSize += fs::file_size(entry.path());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (fs::is_regular_file(inputPath)) {
|
||||||
|
std::string packPath = inputPath.filename().string();
|
||||||
|
|
||||||
|
if (opts.verbose) {
|
||||||
|
std::cout << "添加文件: " << packPath << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (builder.addFile(inputPath.string(), packPath)) {
|
||||||
|
++totalFiles;
|
||||||
|
totalSize += fs::file_size(inputPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalFiles == 0) {
|
||||||
|
std::cerr << "错误: 没有文件被添加到资源包\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.verbose) {
|
||||||
|
std::cout << "\n构建资源包: " << opts.outputFile << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!builder.build(opts.outputFile)) {
|
||||||
|
std::cerr << "错误: 构建资源包失败\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto endTime = std::chrono::high_resolution_clock::now();
|
||||||
|
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(endTime - startTime);
|
||||||
|
|
||||||
|
size_t packSize = fs::file_size(opts.outputFile);
|
||||||
|
double ratio = totalSize > 0 ? (100.0 * packSize / totalSize) : 0.0;
|
||||||
|
|
||||||
|
std::cout << "\n完成!\n";
|
||||||
|
std::cout << " 文件数量: " << totalFiles << "\n";
|
||||||
|
std::cout << " 原始大小: " << formatSize(totalSize) << "\n";
|
||||||
|
std::cout << " 压缩大小: " << formatSize(packSize) << "\n";
|
||||||
|
std::cout << " 压缩比率: " << std::fixed << std::setprecision(1) << ratio << "%\n";
|
||||||
|
std::cout << " 耗时: " << duration.count() << "ms\n";
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 列出资源包内容
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 列出资源包内容
|
||||||
|
*/
|
||||||
|
int listPack(const Options& opts) {
|
||||||
|
if (opts.packFile.empty()) {
|
||||||
|
std::cerr << "错误: 未指定资源包文件\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs::exists(opts.packFile)) {
|
||||||
|
std::cerr << "错误: 资源包文件不存在: " << opts.packFile << "\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
extra2d::AssetPack pack;
|
||||||
|
if (!pack.open(opts.packFile)) {
|
||||||
|
std::cerr << "错误: 无法打开资源包\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& header = pack.header();
|
||||||
|
|
||||||
|
std::cout << "资源包: " << opts.packFile << "\n";
|
||||||
|
std::cout << "----------------------------------------\n";
|
||||||
|
std::cout << "版本: " << header.version << "\n";
|
||||||
|
std::cout << "条目数: " << pack.count() << "\n";
|
||||||
|
std::cout << "----------------------------------------\n\n";
|
||||||
|
|
||||||
|
auto assets = pack.assets();
|
||||||
|
|
||||||
|
std::cout << std::left << std::setw(40) << "路径"
|
||||||
|
<< std::right << std::setw(12) << "大小" << "\n";
|
||||||
|
std::cout << std::string(52, '-') << "\n";
|
||||||
|
|
||||||
|
size_t totalSize = 0;
|
||||||
|
for (const auto& id : assets) {
|
||||||
|
auto* entry = pack.getEntry(id);
|
||||||
|
if (entry) {
|
||||||
|
std::cout << std::left << std::setw(40) << id.path
|
||||||
|
<< std::right << std::setw(12) << formatSize(entry->size) << "\n";
|
||||||
|
totalSize += entry->size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << std::string(52, '-') << "\n";
|
||||||
|
std::cout << std::left << std::setw(40) << "总计"
|
||||||
|
<< std::right << std::setw(12) << formatSize(totalSize) << "\n";
|
||||||
|
|
||||||
|
pack.close();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 提取资源包
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 提取资源包内容
|
||||||
|
*/
|
||||||
|
int extractPack(const Options& opts) {
|
||||||
|
if (opts.packFile.empty()) {
|
||||||
|
std::cerr << "错误: 未指定资源包文件\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.outputDir.empty()) {
|
||||||
|
std::cerr << "错误: 未指定输出目录\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs::exists(opts.packFile)) {
|
||||||
|
std::cerr << "错误: 资源包文件不存在: " << opts.packFile << "\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
extra2d::AssetPack pack;
|
||||||
|
if (!pack.open(opts.packFile)) {
|
||||||
|
std::cerr << "错误: 无法打开资源包\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::path outputDir(opts.outputDir);
|
||||||
|
|
||||||
|
if (!fs::exists(outputDir)) {
|
||||||
|
fs::create_directories(outputDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto assets = pack.assets();
|
||||||
|
size_t extracted = 0;
|
||||||
|
size_t failed = 0;
|
||||||
|
|
||||||
|
std::cout << "提取资源包到: " << opts.outputDir << "\n\n";
|
||||||
|
|
||||||
|
for (const auto& id : assets) {
|
||||||
|
fs::path outputPath = outputDir / id.path;
|
||||||
|
fs::path parentDir = outputPath.parent_path();
|
||||||
|
|
||||||
|
if (!parentDir.empty() && !fs::exists(parentDir)) {
|
||||||
|
fs::create_directories(parentDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto data = pack.read(id);
|
||||||
|
|
||||||
|
if (data.empty()) {
|
||||||
|
std::cerr << "警告: 无法读取: " << id.path << "\n";
|
||||||
|
++failed;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ofstream outFile(outputPath, std::ios::binary);
|
||||||
|
if (!outFile) {
|
||||||
|
std::cerr << "警告: 无法创建文件: " << outputPath.string() << "\n";
|
||||||
|
++failed;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
outFile.write(reinterpret_cast<const char*>(data.data()), data.size());
|
||||||
|
|
||||||
|
if (opts.verbose) {
|
||||||
|
std::cout << " 提取: " << id.path << " (" << formatSize(data.size()) << ")\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
++extracted;
|
||||||
|
}
|
||||||
|
|
||||||
|
pack.close();
|
||||||
|
|
||||||
|
std::cout << "\n完成! 提取 " << extracted << " 个文件";
|
||||||
|
if (failed > 0) {
|
||||||
|
std::cout << ", " << failed << " 个失败";
|
||||||
|
}
|
||||||
|
std::cout << "\n";
|
||||||
|
|
||||||
|
return failed > 0 ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 主函数
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#include <iomanip>
|
||||||
|
|
||||||
|
int main(int argc, char* argv[]) {
|
||||||
|
Options opts;
|
||||||
|
|
||||||
|
if (!parseArgs(argc, argv, opts)) {
|
||||||
|
printHelp(argv[0]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.showHelp) {
|
||||||
|
printHelp(argv[0]);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.command == "create") {
|
||||||
|
return createPack(opts);
|
||||||
|
} else if (opts.command == "list") {
|
||||||
|
return listPack(opts);
|
||||||
|
} else if (opts.command == "extract") {
|
||||||
|
return extractPack(opts);
|
||||||
|
} else {
|
||||||
|
std::cerr << "错误: 未知命令: " << opts.command << "\n";
|
||||||
|
printHelp(argv[0]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
97
xmake.lua
97
xmake.lua
|
|
@ -87,6 +87,9 @@ if target_plat ~= "switch" then
|
||||||
add_requires("glm")
|
add_requires("glm")
|
||||||
add_requires("nlohmann_json")
|
add_requires("nlohmann_json")
|
||||||
add_requires("glfw")
|
add_requires("glfw")
|
||||||
|
add_requires("zstd")
|
||||||
|
add_requires("lz4")
|
||||||
|
add_requires("zlib")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -99,3 +102,97 @@ includes("xmake/engine.lua")
|
||||||
|
|
||||||
-- 定义引擎库
|
-- 定义引擎库
|
||||||
define_extra2d_engine()
|
define_extra2d_engine()
|
||||||
|
|
||||||
|
-- ==============================================
|
||||||
|
-- 测试目标
|
||||||
|
-- ==============================================
|
||||||
|
|
||||||
|
target("extra2d_tests")
|
||||||
|
set_kind("binary")
|
||||||
|
set_default(false)
|
||||||
|
|
||||||
|
-- 测试源文件
|
||||||
|
add_files("Tests/test_framework.cpp")
|
||||||
|
add_files("Tests/test_main.cpp")
|
||||||
|
add_files("Tests/test_data_processor.cpp")
|
||||||
|
add_files("Tests/test_asset_cache.cpp")
|
||||||
|
add_files("Tests/test_asset_pack.cpp")
|
||||||
|
add_files("Tests/test_asset_service.cpp")
|
||||||
|
|
||||||
|
-- 头文件路径
|
||||||
|
add_includedirs("Extra2D/include", {public = true})
|
||||||
|
add_includedirs("Tests")
|
||||||
|
|
||||||
|
-- 依赖引擎库
|
||||||
|
add_deps("extra2d")
|
||||||
|
|
||||||
|
-- 平台配置
|
||||||
|
local plat = get_config("plat") or os.host()
|
||||||
|
|
||||||
|
if plat == "mingw" or plat == "windows" then
|
||||||
|
add_packages("glm", "nlohmann_json", "glfw", "zstd", "lz4", "zlib")
|
||||||
|
add_syslinks("winmm", "imm32", "version", "setupapi")
|
||||||
|
elseif plat == "linux" then
|
||||||
|
add_packages("glm", "nlohmann_json", "glfw", "zstd", "lz4", "zlib")
|
||||||
|
add_syslinks("dl", "pthread")
|
||||||
|
elseif plat == "macosx" then
|
||||||
|
add_packages("glm", "nlohmann_json", "glfw", "zstd", "lz4", "zlib")
|
||||||
|
add_frameworks("Cocoa", "IOKit", "CoreVideo")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 编译器标志
|
||||||
|
add_cxflags("-Wall", "-Wextra", {force = true})
|
||||||
|
add_cxflags("-Wno-unused-variable", "-Wno-unused-function", "-Wno-unused-parameter", {force = true})
|
||||||
|
|
||||||
|
if is_mode("debug") then
|
||||||
|
add_defines("_DEBUG")
|
||||||
|
add_cxxflags("-O0", "-g", {force = true})
|
||||||
|
else
|
||||||
|
add_defines("NDEBUG")
|
||||||
|
add_cxxflags("-O2", {force = true})
|
||||||
|
end
|
||||||
|
target_end()
|
||||||
|
|
||||||
|
-- ==============================================
|
||||||
|
-- 资源打包工具
|
||||||
|
-- ==============================================
|
||||||
|
|
||||||
|
target("asset_packer")
|
||||||
|
set_kind("binary")
|
||||||
|
set_default(false)
|
||||||
|
|
||||||
|
-- 源文件
|
||||||
|
add_files("Tools/asset_packer.cpp")
|
||||||
|
|
||||||
|
-- 头文件路径
|
||||||
|
add_includedirs("Extra2D/include")
|
||||||
|
|
||||||
|
-- 依赖引擎库
|
||||||
|
add_deps("extra2d")
|
||||||
|
|
||||||
|
-- 平台配置
|
||||||
|
local plat = get_config("plat") or os.host()
|
||||||
|
|
||||||
|
if plat == "mingw" or plat == "windows" then
|
||||||
|
add_packages("glm", "nlohmann_json", "glfw", "zstd", "lz4", "zlib")
|
||||||
|
add_syslinks("winmm", "imm32", "version", "setupapi")
|
||||||
|
elseif plat == "linux" then
|
||||||
|
add_packages("glm", "nlohmann_json", "glfw", "zstd", "lz4", "zlib")
|
||||||
|
add_syslinks("dl", "pthread")
|
||||||
|
elseif plat == "macosx" then
|
||||||
|
add_packages("glm", "nlohmann_json", "glfw", "zstd", "lz4", "zlib")
|
||||||
|
add_frameworks("Cocoa", "IOKit", "CoreVideo")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 编译器标志
|
||||||
|
add_cxflags("-Wall", "-Wextra", {force = true})
|
||||||
|
add_cxflags("-Wno-unused-variable", "-Wno-unused-function", "-Wno-unused-parameter", {force = true})
|
||||||
|
|
||||||
|
if is_mode("debug") then
|
||||||
|
add_defines("_DEBUG")
|
||||||
|
add_cxxflags("-O0", "-g", {force = true})
|
||||||
|
else
|
||||||
|
add_defines("NDEBUG")
|
||||||
|
add_cxxflags("-O2", {force = true})
|
||||||
|
end
|
||||||
|
target_end()
|
||||||
|
|
|
||||||
|
|
@ -39,18 +39,21 @@ function define_extra2d_engine()
|
||||||
add_syslinks("glfw", "nx", "m", {public = true})
|
add_syslinks("glfw", "nx", "m", {public = true})
|
||||||
elseif plat == "mingw" or plat == "windows" then
|
elseif plat == "mingw" or plat == "windows" then
|
||||||
-- Windows (MinGW) 平台配置
|
-- Windows (MinGW) 平台配置
|
||||||
add_packages("glm", "nlohmann_json", "glfw", {public = true})
|
add_packages("glm", "nlohmann_json", "glfw", "zstd", "lz4", "zlib", {public = true})
|
||||||
add_syslinks("winmm", "imm32", "version", "setupapi", {public = true})
|
add_syslinks("winmm", "imm32", "version", "setupapi", {public = true})
|
||||||
elseif plat == "linux" then
|
elseif plat == "linux" then
|
||||||
-- Linux 平台配置
|
-- Linux 平台配置
|
||||||
add_packages("glm", "nlohmann_json", "glfw", {public = true})
|
add_packages("glm", "nlohmann_json", "glfw", "zstd", "lz4", "zlib", {public = true})
|
||||||
add_syslinks("dl", "pthread", {public = true})
|
add_syslinks("dl", "pthread", {public = true})
|
||||||
elseif plat == "macosx" then
|
elseif plat == "macosx" then
|
||||||
-- macOS 平台配置
|
-- macOS 平台配置
|
||||||
add_packages("glm", "nlohmann_json", "glfw", {public = true})
|
add_packages("glm", "nlohmann_json", "glfw", "zstd", "lz4", "zlib", {public = true})
|
||||||
add_frameworks("Cocoa", "IOKit", "CoreVideo", {public = true})
|
add_frameworks("Cocoa", "IOKit", "CoreVideo", {public = true})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- 启用压缩库
|
||||||
|
add_defines("E2D_USE_ZSTD", "E2D_USE_LZ4", "E2D_USE_ZLIB", {public = true})
|
||||||
|
|
||||||
-- 编译器标志 (C 和 C++ 共用)
|
-- 编译器标志 (C 和 C++ 共用)
|
||||||
add_cxflags("-Wall", "-Wextra", {force = true})
|
add_cxflags("-Wall", "-Wextra", {force = true})
|
||||||
add_cxflags("-Wno-unused-variable", "-Wno-unused-function", "-Wno-unused-parameter", {force = true})
|
add_cxflags("-Wno-unused-variable", "-Wno-unused-function", "-Wno-unused-parameter", {force = true})
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue