mirror of
https://gitee.com/ulthon/ulthon_admin.git
synced 2026-08-30 12:45:32 +08:00
feat(test): 引入测试规则文档与参数化测试库初始化脚本
对齐 wkbox 衍生项目的测试规则体系,剥离业务特定内容后移植到框架:
- 规则 .agents/rules/ulthon-testing.md(设计哲学/决策树/4层模型/安全规范/fixture原则)
- 技能 .agents/skills/ulthon-testing(运行/编写/模板/决策/回归验证)
- 参数化建库脚本 tests/setup_test_db.{php,ps1,sh} + README
- AGENTS.md 索引补充
与 wkbox 差异:测试库参数化(复用 .env 连接,database=env+'_test'),不硬编码 Docker;删去 canBind Oracle 业务约束与具体 fixture 工厂,业务工厂由衍生项目自行实现。
This commit is contained in:
176
tests/README.md
Normal file
176
tests/README.md
Normal file
@@ -0,0 +1,176 @@
|
||||
# 测试数据库初始化
|
||||
|
||||
本目录提供测试数据库(默认 `ulthon_admin_test`)的初始化脚本。运行一次即可在与 `.env`
|
||||
同实例的 MySQL 上建出框架运行所需的**全部表**(系统表 + 业务表),并插入最小参考数据。
|
||||
|
||||
## 测试库策略:参数化,无需专用 Docker 容器
|
||||
|
||||
ulthon 是框架仓库,测试库策略**与衍生项目(wkbox)不同**:
|
||||
|
||||
- **完全参数化**:连接参数(hostname / hostport / username / password / charset /
|
||||
prefix / database)全部从 `.env` 读取,复用开发库同实例的 MySQL。
|
||||
- **不依赖 Docker**:单机装一个 MySQL 即可,不需要为测试专起容器。
|
||||
- **测试库名派生规则**:`.env` 的 `DATABASE` + `_test` 后缀。如 `DATABASE=ulthon_admin`
|
||||
则测试库为 `ulthon_admin_test`;若 `.env` 改成别的项目名,测试库名会跟着变。
|
||||
- **脚本自带建库**:用 PDO 直连 MySQL 执行 `CREATE DATABASE IF NOT EXISTS`,测试库
|
||||
不存在会自动创建(utf8mb4 / utf8mb4_unicode_ci),已存在则幂等跳过。
|
||||
|
||||
> 衍生项目(如 wkbox)如果偏好专用 Docker 容器做物理隔离,可以 fork 本脚本,把 PDO
|
||||
> 建库段换成 `docker exec ... mysql -e "..."`,并把连接参数改成硬编码容器端口——
|
||||
> 但 ulthon 框架自身保持参数化、零容器假设。
|
||||
|
||||
## 文件说明
|
||||
|
||||
| 文件 | 作用 |
|
||||
|------|------|
|
||||
| `setup_test_db.php` | 真正的运行器(PHP)。启动 ThinkPHP 容器、读 `.env`、PDO 建测试库、config 级覆盖连接、按序调用 `migrate:run` / `scheme:sync` / `seed:run`、查 `information_schema` 自验证。 |
|
||||
| `setup_test_db.ps1` | Windows / PowerShell 包装器:检测 php 在 PATH → 调 PHP 运行器 → 透传退出码。 |
|
||||
| `setup_test_db.sh` | bash / CI 包装器:逻辑同上,Linux / macOS / CI 用。 |
|
||||
| `bootstrap.php` | PHPUnit 引导脚本(已存在,独立维护)。连接覆盖逻辑与本脚本完全一致,二者必须保持同步——否则 PHPUnit 跑的测试库与本脚本初始化的不是同一个。 |
|
||||
|
||||
## 如何运行
|
||||
|
||||
```powershell
|
||||
# Windows / PowerShell
|
||||
powershell -File tests/setup_test_db.ps1
|
||||
# 或直接跑运行器
|
||||
php tests/setup_test_db.php
|
||||
```
|
||||
|
||||
```bash
|
||||
# Linux / macOS / CI
|
||||
bash tests/setup_test_db.sh
|
||||
# 或
|
||||
php tests/setup_test_db.php
|
||||
```
|
||||
|
||||
成功后测试库(如 `ulthon_admin_test`)会包含框架所需的系统表(`ul_system_*`)以及
|
||||
`app/admin/scheme/` 下声明的业务表,退出码为 `0`。脚本自带验证段,会打印表总数并按
|
||||
`system` / `app_` / 其它分类,并断言关键系统表存在。
|
||||
|
||||
## 关键概念:migrate:run 与 scheme:sync 两套机制,缺一不可
|
||||
|
||||
ulthon 的建表有**两个独立的来源**,二者不能互相替代:
|
||||
|
||||
| 机制 | 命令 | 来源目录 | 建什么表 | 举例 |
|
||||
|------|------|----------|----------|------|
|
||||
| Phinx 迁移 | `php think migrate:run` | `database/migrations/` | 系统 / 框架基础表 | `ul_system_admin`、`ul_system_menu`、`ul_system_auth_node`、`ul_system_config`、`ul_system_host`、`ul_system_timer_*`、`ul_debug_log` 等 |
|
||||
| Scheme 同步 | `php think scheme:sync` | `app/admin/scheme/` | 业务表(用 PHP 8 Attribute 声明) | 框架仓库本身不带业务表,使用方按需在 `app/admin/scheme/` 添加 |
|
||||
|
||||
- **只跑 `migrate:run`**:得到系统表,但没有业务表。
|
||||
- **只跑 `scheme:sync`**:得到业务表,但没有系统表,框架无法登录、无法鉴权。
|
||||
- **必须两个都跑**,业务表通常引用系统表(如外键关联 `system_admin`)。
|
||||
|
||||
`seed:run` 负责最小参考数据(超级管理员、权限节点、系统设置、菜单、快捷入口等),由
|
||||
`database/seeds/InitBaseAdminData.php` 提供,自带 install-lock,重复执行安全跳过。
|
||||
|
||||
本脚本在**同一进程**内依次调用这三条命令(见下节),确保它们都指向测试库。
|
||||
|
||||
## 技术细节:为什么用 config 级覆盖,而不是环境变量
|
||||
|
||||
ThinkPHP 的 `Env` 组件加载 `.env` 文件,且 **`.env` 的值优先于 OS 环境变量**
|
||||
(`getenv` 仅作为 `.env` 中不存在键的 fallback)。因此下列方式都**无法**覆盖 `.env`
|
||||
里已存在的连接键(`HOSTNAME` / `DATABASE` / `USERNAME` / `PASSWORD` / `HOSTPORT` 都在
|
||||
`.env` 中):
|
||||
|
||||
- `putenv('DATABASE=ulthon_admin_test')`(PHP)
|
||||
- `$env:DATABASE='ulthon_admin_test'`(PowerShell)
|
||||
- `DATABASE=ulthon_admin_test php think migrate:run`(bash 前缀变量)
|
||||
|
||||
**唯一可靠**的覆盖点是 **config 级**:在 `App::initialize()` 之后,直接改写
|
||||
`config/database.php` 解析出的连接配置数组。因为 `Db` facade 运行时读 config,
|
||||
config 被改写后,后续所有 DB 操作(包括 `migrate:run` / `scheme:sync` / `seed:run`
|
||||
内部)都连到测试库。
|
||||
|
||||
`setup_test_db.php` 的核心步骤:
|
||||
|
||||
```php
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
$app = new \think\App();
|
||||
$app->initialize();
|
||||
|
||||
// 从 .env 读连接参数(与 bootstrap.php 完全一致)
|
||||
$hostname = \think\facade\Env::get('database.hostname');
|
||||
// ...其它参数同理
|
||||
$testDatabase = \think\facade\Env::get('database.database') . '_test';
|
||||
|
||||
// PDO 直连(不指定 database),建测试库
|
||||
$pdo = new \PDO("mysql:host={$hostname};port={$hostport};charset={$charset}", $username, $password);
|
||||
$pdo->exec("CREATE DATABASE IF NOT EXISTS `{$testDatabase}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
|
||||
|
||||
// 把整段 'database' 配置 pull 出来,原地改 connections.main,再 set 回去。
|
||||
// 注意:think\Config::set(array, $name) 的 $name 只支持单段一级名,
|
||||
// 不像 Config::get 那样解析点号多级路径——不能直接传 'database.connections.main'。
|
||||
$dbConfig = \think\facade\Config::get('database');
|
||||
$dbConfig['default'] = 'main';
|
||||
$dbConfig['connections']['main'] = array_merge(
|
||||
$dbConfig['connections']['main'] ?? [],
|
||||
[
|
||||
'hostname' => $hostname, 'hostport' => $hostport,
|
||||
'database' => $testDatabase, 'username' => $username, 'password' => $password,
|
||||
'charset' => $charset, 'prefix' => $prefix, 'fields_cache' => false,
|
||||
],
|
||||
['query' => \app\common\provider\db\Query::class], // 必须保留框架自定义查询类
|
||||
);
|
||||
\think\facade\Config::set($dbConfig, 'database');
|
||||
|
||||
// 安全护栏:库名必须含 'test' 才允许继续
|
||||
if (stripos(\think\facade\Config::get('database.connections.main.database'), 'test') === false) {
|
||||
throw new \RuntimeException('拒绝执行:当前库不像测试库');
|
||||
}
|
||||
|
||||
// 同进程内调用 think 命令,config 覆盖对它们同样生效
|
||||
\think\facade\Console::call('migrate:run', ['--force-force']);
|
||||
\think\facade\Console::call('scheme:sync', ['--force-force']); // --force-force 跳过交互确认
|
||||
\think\facade\Console::call('seed:run', ['--force-force']);
|
||||
```
|
||||
|
||||
### 关于 `--force-force`(`-ff`)
|
||||
|
||||
`scheme:sync` 在检测到 Scheme 与 DB 有差异时会**交互式确认**("确认要将这些变更应用到
|
||||
数据库吗?")。非 TTY 环境(脚本、CI)下 `think\console\Output::confirm` 会抛
|
||||
"Aborted" 而失败。框架提供了全局 `--force-force` / `-ff` 选项(见
|
||||
`extend/base/common/provider/ConsoleBase.php`、`extend/base/common/console/OutputBase.php`),
|
||||
命中时 `confirm()` 直接返回默认值,跳过所有交互确认。本脚本对三条命令统一带上该参数,
|
||||
保证非交互可运行、可重跑。
|
||||
|
||||
### 关于默认连接名 `main`
|
||||
|
||||
本项目 `config/database.php` 的默认连接是 `main`(不是 ThinkPHP 常见的 `mysql`),
|
||||
`default = Env::get('database.main')`,`.env` 里 `MAIN=main`。因此覆盖目标是
|
||||
`connections.main`,**不是** `connections.mysql`。
|
||||
|
||||
## 幂等性
|
||||
|
||||
脚本可安全重复执行:
|
||||
|
||||
- `migrate:run`:已执行的迁移记录在 `ul_migrations`(phinxlog),二次运行直接 "All Done" 无操作。
|
||||
- `scheme:sync`:无差异时打印 "未检测到 Scheme 变更";有差异时先备份原表(`ul_backup_<时间戳>_<表名>`)再改,不报错。
|
||||
- `seed:run`:`InitBaseAdminData` 自带 install-lock(`base_admin_install` 配置项),二次运行打印 "系统已初始化,跳过当前程序"。
|
||||
- `CREATE DATABASE IF NOT EXISTS`:库已存在时无操作。
|
||||
|
||||
> 注:`scheme:sync` 每次遇到差异都会生成 `ul_backup_*` 备份表,这是框架的设计行为
|
||||
> (改表结构前自动备份)。若测试库中积累了大量备份表,可直接 `DROP DATABASE` +
|
||||
> `CREATE DATABASE` 后重跑本脚本重建(见下)。
|
||||
|
||||
## 彻底重置测试库
|
||||
|
||||
当测试库状态污染、备份表堆积、或想从干净状态重新开始时:
|
||||
|
||||
```bash
|
||||
# 用 .env 里的 root 账号连本机 MySQL(按实际 .env 替换 host/port/user/pass)
|
||||
mysql -h127.0.0.1 -P3306 -uroot -proot \
|
||||
-e "DROP DATABASE IF EXISTS ulthon_admin_test; \
|
||||
CREATE DATABASE ulthon_admin_test CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
|
||||
# 然后重跑初始化
|
||||
php tests/setup_test_db.php
|
||||
```
|
||||
|
||||
PowerShell 等价:
|
||||
|
||||
```powershell
|
||||
mysql -h127.0.0.1 -P3306 -uroot -proot `
|
||||
-e "DROP DATABASE IF EXISTS ulthon_admin_test; `
|
||||
CREATE DATABASE ulthon_admin_test CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
|
||||
php tests/setup_test_db.php
|
||||
```
|
||||
212
tests/setup_test_db.php
Normal file
212
tests/setup_test_db.php
Normal file
@@ -0,0 +1,212 @@
|
||||
<?php
|
||||
/**
|
||||
* 测试数据库初始化脚本(参数化版).
|
||||
*
|
||||
* 在与 .env 同实例的 MySQL 上建出 ulthon_admin_test 库,并初始化全部表:
|
||||
* 1. migrate:run 建系统/框架表(database/migrations/,如 ul_system_admin / ul_system_menu)
|
||||
* 2. scheme:sync 建业务表(app/admin/scheme/)
|
||||
* 3. seed:run 插入最小参考数据(database/seeds/InitBaseAdminData)
|
||||
*
|
||||
* 与 wkbox 衍生项目的区别:
|
||||
* - 完全参数化:连接参数(hostname/hostport/username/password/charset/prefix/database)
|
||||
* 全部从 .env 读取,复用开发库同实例的 MySQL,不依赖专用 Docker 容器,不硬编码端口。
|
||||
* - 测试库名 = .env 中 DATABASE + '_test' 后缀(如 ulthon_admin → ulthon_admin_test)。
|
||||
* - 脚本本身负责 CREATE DATABASE IF NOT EXISTS(单机 MySQL,库可能尚未创建)。
|
||||
*
|
||||
* 连接覆盖逻辑与 tests/bootstrap.php 完全一致(必须保持同步,否则脚本与 PHPUnit 跑的
|
||||
* 不是同一个测试库)。关键技术点见 tests/README.md「技术细节」一节:
|
||||
* - ThinkPHP 的 Env 加载 .env 且 .env 值优先于 OS 环境变量,putenv/$env:VAR/bash 前缀
|
||||
* 都无法覆盖已存在的 .env 键,唯一可靠覆盖点是 config 级。
|
||||
* - think\Config::set 的第二参数只支持单段一级名,不能传 'database.connections.main'。
|
||||
*
|
||||
* 幂等:migrate:run / scheme:sync / seed:run 均可安全重复执行(详见 README)。
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// 1. 启动 ThinkPHP 容器(必须先 require autoload,再 initialize)
|
||||
// ----------------------------------------------------------------------------
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
$app = new \think\App();
|
||||
$app->initialize();
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// 2. 从 .env 读取连接参数,组装测试库连接配置
|
||||
// 与 tests/bootstrap.php 完全一致:复用开发库同实例连接,仅替换 database 名。
|
||||
// ----------------------------------------------------------------------------
|
||||
$envHostname = \think\facade\Env::get('database.hostname');
|
||||
$envHostport = \think\facade\Env::get('database.hostport');
|
||||
$envUsername = \think\facade\Env::get('database.username');
|
||||
$envPassword = \think\facade\Env::get('database.password');
|
||||
$envCharset = \think\facade\Env::get('database.charset');
|
||||
$envPrefix = \think\facade\Env::get('database.prefix');
|
||||
$envDatabase = \think\facade\Env::get('database.database');
|
||||
|
||||
// 测试库名策略:原库名 + _test 后缀(如 ulthon_admin → ulthon_admin_test)
|
||||
$testDatabase = is_string($envDatabase) && $envDatabase !== ''
|
||||
? $envDatabase . '_test'
|
||||
: 'ulthon_admin_test';
|
||||
|
||||
$hostname = is_string($envHostname) && $envHostname !== '' ? $envHostname : '127.0.0.1';
|
||||
$hostport = is_string($envHostport) && $envHostport !== '' ? $envHostport : '3306';
|
||||
$username = is_string($envUsername) && $envUsername !== '' ? $envUsername : 'root';
|
||||
$password = is_string($envPassword) ? $envPassword : '';
|
||||
$charset = is_string($envCharset) && $envCharset !== '' ? $envCharset : 'utf8mb4';
|
||||
$prefix = is_string($envPrefix) && $envPrefix !== '' ? $envPrefix : 'ul_';
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// 3. 早期安全护栏:解析出的库名必须含 'test',否则拒绝继续
|
||||
// (在任何写操作之前触发,避免误连开发库)
|
||||
// ----------------------------------------------------------------------------
|
||||
if (stripos($testDatabase, 'test') === false) {
|
||||
throw new \RuntimeException("拒绝执行:解析到的测试库名 [{$testDatabase}] 不含 'test',已中止。");
|
||||
}
|
||||
|
||||
fwrite(STDOUT, "==== 测试库目标:{$testDatabase} @ {$hostname}:{$hostport}(user: {$username}) ====\n");
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// 4. 确保测试库存在(用 PDO 直连,不指定 database,执行 CREATE DATABASE IF NOT EXISTS)
|
||||
// 单机 MySQL 场景下测试库可能尚未创建;Docker 预 provision 的库也无妨,IF NOT EXISTS 幂等。
|
||||
// ----------------------------------------------------------------------------
|
||||
try {
|
||||
$dsn = "mysql:host={$hostname};port={$hostport};charset={$charset}";
|
||||
$pdo = new \PDO($dsn, $username, $password, [
|
||||
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
|
||||
]);
|
||||
$quotedDb = '`' . str_replace('`', '``', $testDatabase) . '`';
|
||||
$pdo->exec("CREATE DATABASE IF NOT EXISTS {$quotedDb} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
|
||||
fwrite(STDOUT, "==== 确保 TEST_DB 已存在:{$testDatabase}(utf8mb4 / utf8mb4_unicode_ci) ====\n");
|
||||
} catch (\Throwable $e) {
|
||||
fwrite(STDERR, "[ERROR] 创建测试库失败:{$e->getMessage()}\n");
|
||||
fwrite(STDERR, " 请检查 .env 中 HOSTNAME/HOSTPORT/USERNAME/PASSWORD 是否能连上 MySQL。\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// 5. config 级覆盖 connections.main 指向测试库
|
||||
// 覆盖目标 = connections.main(本项目 config/database.php 的默认连接名是 main)。
|
||||
// 坑点:think\Config::set(array, $name) 的 $name 只支持单段一级配置名,
|
||||
// 不像 Config::get 那样解析点号多级路径。正确做法:把整段 'database' 配置 pull 出来,
|
||||
// 原地改 connections.main,再 Config::set($dbConfig, 'database') 写回。
|
||||
// 必须保留 ['query' => \app\common\provider\db\Query::class],否则 ORM 行为异常。
|
||||
// ----------------------------------------------------------------------------
|
||||
$dbConfig = \think\facade\Config::get('database');
|
||||
if (!is_array($dbConfig)) {
|
||||
$dbConfig = [];
|
||||
}
|
||||
$dbConfig['default'] = 'main';
|
||||
if (!isset($dbConfig['connections']) || !is_array($dbConfig['connections'])) {
|
||||
$dbConfig['connections'] = [];
|
||||
}
|
||||
if (!isset($dbConfig['connections']['main']) || !is_array($dbConfig['connections']['main'])) {
|
||||
$dbConfig['connections']['main'] = [];
|
||||
}
|
||||
$dbConfig['connections']['main'] = array_merge(
|
||||
$dbConfig['connections']['main'],
|
||||
[
|
||||
'type' => 'mysql',
|
||||
'hostname' => $hostname,
|
||||
'hostport' => $hostport,
|
||||
'database' => $testDatabase,
|
||||
'username' => $username,
|
||||
'password' => $password,
|
||||
'charset' => $charset,
|
||||
'prefix' => $prefix,
|
||||
'fields_cache' => false,
|
||||
],
|
||||
// 必须保留框架自定义 query 类,否则 ORM 行为异常。
|
||||
['query' => \app\common\provider\db\Query::class],
|
||||
);
|
||||
\think\facade\Config::set($dbConfig, 'database');
|
||||
|
||||
// 二次护栏:写回 config 之后再校验一次,确认覆盖真的生效。
|
||||
$resolvedDb = \think\facade\Config::get('database.connections.main.database');
|
||||
if (!is_string($resolvedDb) || stripos($resolvedDb, 'test') === false) {
|
||||
throw new \RuntimeException('Config 覆盖未生效,resolved database = ' . var_export($resolvedDb, true));
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// 6. 逐条执行建表/建库命令(同进程 Console::call,config 覆盖对命令同样生效)
|
||||
// Console::call 返回 think\console\Output,命令输出被写进其私有 handle(Buffer)。
|
||||
// 用反射取出 Buffer 内容回显,便于排查;同时去掉 ANSI 着色码避免终端转义污染日志。
|
||||
// --force-force 是框架全局选项,命中时 confirm() 直接返回默认值,跳过所有交互确认——
|
||||
// scheme:sync 在非 TTY 下不带它会被 parent::confirm 抛 "Aborted"。
|
||||
// ----------------------------------------------------------------------------
|
||||
$commands = [
|
||||
// 系统/框架表(Phinx 迁移):ul_system_admin / ul_system_menu / ul_system_auth_node / ul_system_config / ...
|
||||
['migrate:run', '系统/框架表(database/migrations/)'],
|
||||
// 业务表(Scheme 同步):app/admin/scheme/ 下声明的表
|
||||
['scheme:sync', '业务表(app/admin/scheme/)'],
|
||||
// 参考数据(seed):InitBaseAdminData,自带 install-lock 跳过,可安全重跑
|
||||
['seed:run', '参考数据(database/seeds/)'],
|
||||
];
|
||||
|
||||
$handleProp = (new \ReflectionProperty(\think\console\Output::class, 'handle'));
|
||||
$handleProp->setAccessible(true);
|
||||
|
||||
foreach ($commands as [$cmd, $desc]) {
|
||||
fwrite(STDOUT, "\n>>>> [{$cmd}] {$desc}\n");
|
||||
try {
|
||||
/** @var \think\console\Output $output */
|
||||
$output = \think\facade\Console::call($cmd, ['--force-force']);
|
||||
// 取出 Buffer 里命令自身打印的内容并回显
|
||||
$handle = $handleProp->getValue($output);
|
||||
$captured = ($handle instanceof \think\console\output\driver\Buffer) ? $handle->fetch() : '';
|
||||
if ($captured !== '') {
|
||||
// 去掉 ANSI 着色码,避免终端转义污染日志
|
||||
$captured = preg_replace('/\x1b\[[0-9;]*m/', '', $captured);
|
||||
fwrite(STDOUT, rtrim($captured) . "\n");
|
||||
}
|
||||
fwrite(STDOUT, "<<<< [{$cmd}] 完成(无异常)\n");
|
||||
} catch (\Throwable $e) {
|
||||
// 单条命令失败不立刻退出,继续后续命令,最后由验证段汇总。
|
||||
fwrite(STDERR, "<<<< [{$cmd}] 异常:{$e->getMessage()}\n");
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// 7. 验证:直接查 information_schema.tables WHERE table_schema=?,证明表确实落在测试库
|
||||
// ulthon 是框架仓库,没有 app_* 业务表,只断言系统表 ul_system_* 关键四张存在。
|
||||
// ----------------------------------------------------------------------------
|
||||
fwrite(STDOUT, "\n==== 验证:{$resolvedDb} 内表清单 ====\n");
|
||||
|
||||
try {
|
||||
// 用裸 SQL 查 information_schema,绕开 ORM 的前缀/字段缓存 quirks
|
||||
$rows = \think\facade\Db::connect('main')->query(
|
||||
'SELECT table_name AS t FROM information_schema.tables WHERE table_schema = ? ORDER BY table_name',
|
||||
[$resolvedDb],
|
||||
);
|
||||
$names = array_column($rows, 't');
|
||||
|
||||
$totalCount = count($names);
|
||||
fwrite(STDOUT, "表总数:{$totalCount}\n");
|
||||
|
||||
// 分类展示:系统表 vs 业务表 vs 其它(按命名约定 ul_system_* / ul_app_* / 其它)
|
||||
$systemTables = array_values(array_filter($names, static fn ($n) => stripos($n, 'system') !== false));
|
||||
$appTables = array_values(array_filter($names, static fn ($n) => stripos($n, 'app_') !== false));
|
||||
$otherTables = array_values(array_diff($names, $systemTables, $appTables));
|
||||
|
||||
fwrite(STDOUT, "系统表(system,来自 migrate:run):" . (empty($systemTables) ? '(无)' : implode(', ', $systemTables)) . "\n");
|
||||
fwrite(STDOUT, "业务表(app_,来自 scheme:sync):" . (empty($appTables) ? '(无)' : implode(', ', $appTables)) . "\n");
|
||||
if (!empty($otherTables)) {
|
||||
fwrite(STDOUT, "其它表(migrations / debug_log / backup 等):" . implode(', ', $otherTables) . "\n");
|
||||
}
|
||||
|
||||
// 关键系统表必存在断言(ulthon 框架核心:登录、菜单、权限节点、配置)
|
||||
$required = ['ul_system_admin', 'ul_system_menu', 'ul_system_auth_node', 'ul_system_config'];
|
||||
$missing = array_values(array_filter($required, static fn ($n) => !in_array($n, $names, true)));
|
||||
if (!empty($missing)) {
|
||||
fwrite(STDERR, "\n[警告] 缺少关键系统表:" . implode(', ', $missing) . "\n");
|
||||
fwrite(STDERR, " 检查 migrate:run 是否真的连到测试库并成功执行。\n");
|
||||
exit(2);
|
||||
}
|
||||
fwrite(STDOUT, "\n[OK] 关键系统表全部存在:system_admin / system_menu / system_auth_node / system_config\n");
|
||||
} catch (\Throwable $e) {
|
||||
fwrite(STDERR, "验证查询失败:{$e->getMessage()}\n");
|
||||
exit(3);
|
||||
}
|
||||
|
||||
fwrite(STDOUT, "\n==== 测试数据库初始化完成 ====\n");
|
||||
exit(0);
|
||||
46
tests/setup_test_db.ps1
Normal file
46
tests/setup_test_db.ps1
Normal file
@@ -0,0 +1,46 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Test database setup (Windows / PowerShell wrapper).
|
||||
|
||||
.DESCRIPTION
|
||||
Calls the PHP runner tests/setup_test_db.php, which in a single process:
|
||||
1. reads connection params from .env (hostname/hostport/username/password/
|
||||
charset/prefix/database)
|
||||
2. derives the test DB name as {env.database} + '_test'
|
||||
(e.g. ulthon_admin -> ulthon_admin_test)
|
||||
3. CREATE DATABASE IF NOT EXISTS on the same MySQL instance (no Docker)
|
||||
4. config-level override connections.main -> test DB
|
||||
5. php think migrate:run -> system/framework tables (database/migrations/)
|
||||
6. php think scheme:sync -> business tables (app/admin/scheme/)
|
||||
7. php think seed:run -> reference data (database/seeds/)
|
||||
8. queries information_schema to verify the tables landed in the test DB
|
||||
|
||||
Fully parameterized: no hardcoded host/port/credentials. The MySQL instance
|
||||
is whatever .env points at (typically a single local MySQL).
|
||||
|
||||
.NOTES
|
||||
ASCII-only on purpose: PowerShell 5.1 mis-decodes BOM-less UTF-8 CJK.
|
||||
#>
|
||||
|
||||
# Runner lives next to this script.
|
||||
$Runner = Join-Path $PSScriptRoot 'setup_test_db.php'
|
||||
|
||||
# 1. Ensure php is on PATH.
|
||||
$phpCmd = Get-Command php -ErrorAction SilentlyContinue
|
||||
if (-not $phpCmd) {
|
||||
Write-Host "[ERROR] 'php' not found on PATH. Install PHP 8+ and add it to PATH." -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 2. Run the PHP runner.
|
||||
Write-Host "==== Running setup_test_db.php ====" -ForegroundColor Cyan
|
||||
& php $Runner
|
||||
$code = $LASTEXITCODE
|
||||
|
||||
Write-Host ""
|
||||
if ($code -eq 0) {
|
||||
Write-Host "==== Done. PHP exit code = $code ====" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "==== Done. PHP exit code = $code ====" -ForegroundColor Red
|
||||
}
|
||||
exit $code
|
||||
35
tests/setup_test_db.sh
Normal file
35
tests/setup_test_db.sh
Normal file
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env bash
|
||||
# 测试数据库初始化(bash / CI 包装器)。
|
||||
#
|
||||
# 调用 PHP 运行器 tests/setup_test_db.php,在同一进程内:
|
||||
# 1. 从 .env 读连接参数(hostname/hostport/username/password/charset/prefix/database)
|
||||
# 2. 派生测试库名 = {env.database} + '_test'(如 ulthon_admin -> ulthon_admin_test)
|
||||
# 3. 在同一 MySQL 实例上 CREATE DATABASE IF NOT EXISTS(无需专用 Docker 容器)
|
||||
# 4. config 级覆盖 connections.main 指向测试库
|
||||
# 5. php think migrate:run 建系统/框架表(database/migrations/)
|
||||
# 6. php think scheme:sync 建业务表(app/admin/scheme/)
|
||||
# 7. php think seed:run 插参考数据(database/seeds/)
|
||||
# 8. 查 information_schema 验证表已落到测试库
|
||||
#
|
||||
# 完全参数化:不硬编码 host/port/credentials。MySQL 实例 = .env 指向的那个。
|
||||
# 详见 tests/README.md。
|
||||
set -euo pipefail
|
||||
|
||||
# 项目根目录(脚本位于 tests/ 下,取上一级)
|
||||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
RUNNER="$PROJECT_ROOT/tests/setup_test_db.php"
|
||||
|
||||
# 1. 确保 php 在 PATH 上
|
||||
if ! command -v php >/dev/null 2>&1; then
|
||||
echo "[错误] PATH 上找不到 php。请安装 PHP 8+ 并加入 PATH。"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 2. 运行 PHP 运行器
|
||||
echo "==== 运行 setup_test_db.php ===="
|
||||
php "$RUNNER"
|
||||
code=$?
|
||||
|
||||
echo ""
|
||||
echo "==== 完成,PHP 退出码 = $code ===="
|
||||
exit "$code"
|
||||
Reference in New Issue
Block a user