Commit 2d0e8878 authored by nanahira's avatar nanahira

yrp tests

parent 8fcdf430
Pipeline #42893 failed with stages
in 60 minutes and 7 seconds
...@@ -89,3 +89,38 @@ https://cdn02.moecube.com:444/ygopro-super-pre/versions/master/test-release-v2.j ...@@ -89,3 +89,38 @@ https://cdn02.moecube.com:444/ygopro-super-pre/versions/master/test-release-v2.j
- 通知相关人员进行整合到 8888 服务器。 - 通知相关人员进行整合到 8888 服务器。
本方法测试的 BUG 进度在表格的「内核更新测试记录」标签页追踪。 本方法测试的 BUG 进度在表格的「内核更新测试记录」标签页追踪。
### 自动化测试
进行测试之后,请把相关的 yrp 录像文件放置在 `tests/yrp` 目录内。日后系统自动化测试将会运行这些 yrp 回放文件,避免后续重复脚本故障。
如果 yrp 是使用残局运行的,那么请把对应的残局文件放在 `tests/single` 目录内(请不要改残局名字)。
#### 环境准备
如果需要做 yrp 固化或者运行测试本身,那么需要在本目录内准备下列文件,并准备好 Node.js 环境,运行 `npm ci` 安装必要 js 依赖,确保测试框架正常运行。
- `ygopro/cards.cdb`
- `ygopro/script`
不需要额外放入超先行卡数据或者 ypk。超先行卡数据会从根目录读取。
#### yrp 固化
默认情况下,测试 yrp 只测试「YRP 是否能正常运行」,不会测试「能否复现特定的游戏状态」。如果需要测试特定的游戏状态,请使用 `freezeyrp` 脚本把 yrp 固化。
```bash
npm run freezeyrp <name1> <name2> ...
```
例如
```bash
npm run freezeyrp sample
```
这会读取 `tests/yrp/sample.yrp` 文件,在项目内创建 `tests/yrp-info/sample.yaml` 文件,记录 `sample.yrp` 的游戏状态。之后每次运行测试时,系统会把 `sample.yrp` 固化成 `sample.yaml` 记录的状态,进行更为严格的检查。
#### jstest api
对于使用 jstest api 创建的 `spec.ts` 测试文件,请放在 `tests/specs` 目录内。系统会自动运行这些测试文件。
This diff is collapsed.
...@@ -4,7 +4,8 @@ ...@@ -4,7 +4,8 @@
"version": "1.0.0", "version": "1.0.0",
"scripts": { "scripts": {
"lint": "eslint --fix .", "lint": "eslint --fix .",
"test": "jest --passWithNoTests" "test": "jest --passWithNoTests",
"freeze-yrp": "ts-node tests/tools/freeze-yrp.ts"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
...@@ -37,6 +38,7 @@ ...@@ -37,6 +38,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",
"@types/js-yaml": "^4.0.9",
"@types/node": "^25.2.1", "@types/node": "^25.2.1",
"@types/sql.js": "^1.4.9", "@types/sql.js": "^1.4.9",
"@typescript-eslint/eslint-plugin": "^8.54.0", "@typescript-eslint/eslint-plugin": "^8.54.0",
...@@ -47,11 +49,13 @@ ...@@ -47,11 +49,13 @@
"jest": "^30.2.0", "jest": "^30.2.0",
"prettier": "^3.8.1", "prettier": "^3.8.1",
"ts-jest": "^29.4.6", "ts-jest": "^29.4.6",
"ts-node": "^10.9.2",
"typescript": "^5.9.3" "typescript": "^5.9.3"
}, },
"dependencies": { "dependencies": {
"js-yaml": "^4.1.1",
"sql.js": "^1.13.0", "sql.js": "^1.13.0",
"ygopro-jstest": "^1.0.8", "ygopro-jstest": "^1.0.10",
"ygopro-msg-encode": "^1.1.5" "ygopro-msg-encode": "^1.1.5"
} }
} }
Debug.SetAIName("as")
Debug.ReloadFieldBegin(DUEL_ATTACK_FIRST_TURN)
Debug.SetPlayerInfo(0,8000,0,0)
Debug.SetPlayerInfo(1,8000,0,0)
Debug.AddCard(28985331,0,0,LOCATION_HAND,0,POS_FACEUP)
Debug.AddCard(10000000,0,0,LOCATION_HAND,0,POS_FACEUP)
Debug.AddCard(5560911,0,0,LOCATION_DECK,0,POS_FACEDOWN)
Debug.AddCard(14558127,1,1,LOCATION_HAND,0,POS_FACEUP)
Debug.AddCard(73580471,0,0,LOCATION_EXTRA,0,POS_FACEDOWN)
Debug.ReloadFieldEnd()
import {
SlientAdvancor,
SummonPlaceAdvancor,
NoEffectAdvancor,
SelectCardAdvancor,
} from "ygopro-jstest";
import {
OcgcoreScriptConstants,
YGOProMsgSelectIdleCmd,
YGOProMsgDraw,
YGOProMsgSelectEffectYn,
YGOProMsgSelectChain,
} from "ygopro-msg-encode";
import { createTest } from "../utility/create-test";
describe("sample standalone spec", () => {
it("Should process duel", async () => {
await createTest({}, (ctx) =>
ctx
.addCard([
{
code: 28985331,
location: OcgcoreScriptConstants.LOCATION_HAND,
},
{
code: 10000000,
location: OcgcoreScriptConstants.LOCATION_HAND,
},
{
code: 5560911,
location: OcgcoreScriptConstants.LOCATION_DECK,
},
{
code: 14558127,
location: OcgcoreScriptConstants.LOCATION_HAND,
controller: 1,
},
{
code: 73580471,
location: OcgcoreScriptConstants.LOCATION_EXTRA,
},
])
.advance(SlientAdvancor())
.state(YGOProMsgSelectIdleCmd, (msg) => {
expect(
ctx.allMessages.find((m) => m instanceof YGOProMsgDraw),
).toBeUndefined(); // make sure it does not draw any card
const deck = ctx.getFieldCard(
0,
OcgcoreScriptConstants.LOCATION_DECK,
);
expect(deck).toHaveLength(1);
const ex = ctx.getFieldCard(0, OcgcoreScriptConstants.LOCATION_EXTRA);
expect(ex).toHaveLength(1);
const hand = ctx.getFieldCard(
0,
OcgcoreScriptConstants.LOCATION_HAND,
);
expect(hand).toHaveLength(2);
const oppHand = ctx.getFieldCard(
1,
OcgcoreScriptConstants.LOCATION_HAND,
);
expect(oppHand).toHaveLength(1);
const c1 = hand.find((c) => c.code === 28985331);
const c2 = hand.find((c) => c.code === 10000000);
expect(c1).toBeDefined();
expect(c2).toBeDefined();
expect(c1.canSummon()).toBe(true);
expect(c2.canSummon()).toBe(false);
expect(c1.canActivate()).toBe(false);
return c1.summon();
})
.advance(SummonPlaceAdvancor(), NoEffectAdvancor())
.state(YGOProMsgSelectEffectYn, (msg) => {
expect(msg.code).toBe(28985331); // check if it's the correct card effect
return msg.prepareResponse(true);
})
.state(YGOProMsgSelectChain, (msg) => {
const field = ctx.getFieldCard(
1,
OcgcoreScriptConstants.LOCATION_HAND,
);
expect(field).toHaveLength(1);
const c1 = field[0];
expect(c1.code).toBe(14558127);
expect(c1.canActivate()).toBe(true); // can activate urara
// does not activate urara here
})
.advance(SlientAdvancor(), SelectCardAdvancor({ code: 5560911 }))
.state(YGOProMsgSelectIdleCmd, (msg) => {
const grave = ctx.getFieldCard(
0,
OcgcoreScriptConstants.LOCATION_GRAVE,
);
expect(grave).toHaveLength(1);
expect(grave[0].code).toBe(5560911); // the deckdes monster should be in grave
expect(grave[0].canActivate()).toBe(true);
return grave[0].activate();
})
.advance(
SummonPlaceAdvancor(),
SelectCardAdvancor({ code: 28985331 }),
SlientAdvancor(),
)
.state(YGOProMsgSelectIdleCmd, (msg) => {
expect(ctx.getLP(0)).toBe(4000);
const mzone = ctx.getFieldCard(
0,
OcgcoreScriptConstants.LOCATION_MZONE,
);
expect(mzone).toHaveLength(2);
const warrior = mzone.find((c) => c.code === 28985331);
expect(warrior.level).toBe(4);
const dragon = mzone.find((c) => c.code === 5560911);
expect(dragon.level).toBe(3);
const ex = ctx.getFieldCard(0, OcgcoreScriptConstants.LOCATION_EXTRA);
expect(ex).toHaveLength(1);
expect(ex[0].code).toBe(73580471);
expect(ex[0].canSpecialSummon()).toBe(true);
return ex[0].specialSummon();
})
.advance(
SelectCardAdvancor({ code: 5560911 }, { code: 28985331 }),
SummonPlaceAdvancor(),
NoEffectAdvancor(),
)
.state(YGOProMsgSelectEffectYn, (msg) => msg.prepareResponse(true)) // activate effect to destroy itself
.advance(SlientAdvancor())
.state(YGOProMsgSelectIdleCmd, (msg) => {
const mzone = ctx.getFieldCard(
0,
OcgcoreScriptConstants.LOCATION_MZONE,
);
expect(mzone).toHaveLength(0); // destroyed
const grave = ctx.getFieldCard(
0,
OcgcoreScriptConstants.LOCATION_GRAVE,
);
expect(grave).toHaveLength(2); // dragon returned to deck, and black rose and warrior at grave
return;
}),
);
});
});
import path from "node:path";
import fs from "node:fs";
import { createTest } from "../utility/create-test";
import { toYrpInfo } from "../utility/yrp-info";
import yaml from "js-yaml";
async function main() {
const yrpFilenames = process.argv.slice(2);
if (yrpFilenames.length === 0) {
console.error("Usage: npm run yrpfreeze <replay1> <replay2> ...");
process.exit(1);
}
for (const yrpFilename of yrpFilenames) {
const fullPath = path.resolve(
process.cwd(),
"tests",
"yrp",
`${yrpFilename}.yrp`,
);
const destPath = path.resolve(
process.cwd(),
"tests",
"yrp-info",
`${yrpFilename}.yaml`,
);
console.log(`Will save YRP info from ${fullPath} to ${destPath}`);
await createTest({ yrp: fullPath }, async (test) => {
const info = toYrpInfo(test);
console.log(info.snapshotText);
const yamlStr = yaml.dump(info);
await fs.promises.writeFile(destPath, yamlStr, "utf-8");
console.log(`Saved YRP info from ${fullPath} to ${destPath}`);
});
}
}
main().then();
...@@ -11,6 +11,7 @@ export const createTest = ( ...@@ -11,6 +11,7 @@ export const createTest = (
ygoproPath: [ ygoproPath: [
...(options.ygoproPath || []), ...(options.ygoproPath || []),
process.cwd(), process.cwd(),
path.resolve(process.cwd(), "tests"),
path.resolve(process.cwd(), process.env.YGOPRO_PATH || "ygopro"), path.resolve(process.cwd(), process.env.YGOPRO_PATH || "ygopro"),
], ],
}, },
......
import { formatSnapshot, YGOProTest } from "ygopro-jstest";
import { YGOProMsgBase } from "ygopro-msg-encode";
export type MsgSnapshot = {
identifier: number;
msg: string;
} & Partial<YGOProMsgBase>;
export interface YrpInfo {
messages: MsgSnapshot[];
snapshot: ReturnType<typeof YGOProTest.prototype.querySnapshot>;
snapshotText: string;
}
export const toYrpInfo = (test: YGOProTest): YrpInfo => {
const snapshot = test.querySnapshot();
const snapshotText = formatSnapshot(snapshot);
return {
messages: test.allMessages.map((msg) => ({
identifier: msg.identifier,
msg: msg.constructor.name,
...msg,
})),
snapshot,
snapshotText,
};
}
messages:
- identifier: 33
msg: YGOProMsgShuffleHand
player: 0
count: 2
cards:
- 28985331
- 10000000
- identifier: 41
msg: YGOProMsgNewPhase
phase: 1
- identifier: 41
msg: YGOProMsgNewPhase
phase: 2
- identifier: 41
msg: YGOProMsgNewPhase
phase: 4
- identifier: 11
msg: YGOProMsgSelectIdleCmd
player: 0
summonableCount: 1
summonableCards:
- code: 28985331
controller: 0
location: 2
sequence: 0
spSummonableCount: 0
spSummonableCards: []
reposableCount: 0
reposableCards: []
msetableCount: 1
msetableCards:
- code: 28985331
controller: 0
location: 2
sequence: 0
ssetableCount: 0
ssetableCards: []
activatableCount: 0
activatableCards: []
canBp: 1
canEp: 1
canShuffle: 1
snapshot:
cards:
- flags: 15712255
controller: 0
location: 1
sequence: 0
empty: false
code: 5560911
position: 10
alias: 5560911
type: 4129
level: 7
rank: 0
attribute: 32
race: 8192
attack: 1000
defense: 3000
baseAttack: 1000
baseDefense: 3000
reason: 0
targetCards: []
overlayCards: []
counters: []
owner: 0
status: 0
lscale: 0
rscale: 0
link: 0
linkMarker: 0
name: 亡龙之战栗-死欲龙
- flags: 15712255
controller: 0
location: 2
sequence: 0
empty: false
code: 28985331
position: 10
alias: 28985331
type: 33
level: 4
rank: 0
attribute: 32
race: 1
attack: 1400
defense: 1200
baseAttack: 1400
baseDefense: 1200
reason: 0
targetCards: []
overlayCards: []
counters: []
owner: 0
status: 0
lscale: 0
rscale: 0
link: 0
linkMarker: 0
name: 终末之骑士
- flags: 15712255
controller: 0
location: 2
sequence: 1
empty: false
code: 10000000
position: 10
alias: 10000000
type: 33
level: 10
rank: 0
attribute: 64
race: 2097152
attack: 4000
defense: 4000
baseAttack: 4000
baseDefense: 4000
reason: 0
targetCards: []
overlayCards: []
counters: []
owner: 0
status: 0
lscale: 0
rscale: 0
link: 0
linkMarker: 0
name: 欧贝利斯克之巨神兵
- flags: 15712255
controller: 0
location: 64
sequence: 0
empty: false
code: 73580471
position: 10
alias: 73580471
type: 8225
level: 7
rank: 0
attribute: 4
race: 8192
attack: 2400
defense: 1800
baseAttack: 2400
baseDefense: 1800
reason: 0
targetCards: []
overlayCards: []
counters: []
owner: 0
status: 0
lscale: 0
rscale: 0
link: 0
linkMarker: 0
name: 黑蔷薇龙
- flags: 15712255
controller: 1
location: 2
sequence: 0
empty: false
code: 14558127
position: 10
alias: 14558127
type: 4129
level: 3
rank: 0
attribute: 4
race: 16
attack: 0
defense: 1800
baseAttack: 0
baseDefense: 1800
reason: 0
targetCards: []
overlayCards: []
counters: []
owner: 1
status: 0
lscale: 0
rscale: 0
link: 0
linkMarker: 0
name: 灰流丽
lp:
- 8000
- 8000
chains: []
snapshotText: |-
********* Field Snapshot *********
LP: P0 8000 | P1 8000
********* Player 0 *********
Hand: 终末之骑士
欧贝利斯克之巨神兵
********* Player 1 *********
Hand: 灰流丽
********* Finish *********
This diff is collapsed.
import { existsSync, readdirSync } from "node:fs";
import { createTest } from "./utility/create-test";
import { MsgSnapshot, toYrpInfo, YrpInfo } from "./utility/yrp-info";
import yaml from "js-yaml";
import path from "node:path";
import fs from "node:fs";
describe("YRP", () => {
const yrpDirPath = path.resolve(process.cwd(), "tests", "yrp");
const yrpDir = readdirSync(yrpDirPath);
for (const yrpFilename of yrpDir) {
if (!yrpFilename.endsWith(".yrp")) continue;
const testName = `YRP: ${yrpFilename}`;
it(testName, async () => {
const yrpPath = path.resolve(yrpDirPath, yrpFilename);
await createTest({ yrp: yrpPath }, async (test) => {
const yrpInfoPath = path.resolve(
__dirname,
"yrp-info",
yrpFilename.slice(0, -4) + ".yaml",
);
const currentInfo = toYrpInfo(test);
const hasYrpInfo = existsSync(yrpInfoPath);
console.log(
`Testing YRP: ${yrpFilename}\nYRP info yaml: ${hasYrpInfo ? "available" : "none"}\n${currentInfo.snapshotText}`,
);
if (hasYrpInfo) {
// do further tests
const expectedInfo = (await yaml.load(
await fs.promises.readFile(yrpInfoPath, "utf-8"),
)) as YrpInfo;
expect(currentInfo.snapshot.lp).toEqual(expectedInfo.snapshot.lp);
expect(currentInfo.snapshot.chains).toEqual(
expectedInfo.snapshot.chains,
);
expect(currentInfo.snapshot.cards).toEqual(
expectedInfo.snapshot.cards,
);
const sortMesssages = (messages: MsgSnapshot[]) =>
messages
.filter((m) => !m.msg.includes("Hint"))
.map((m) => {
// go through all properties and prune every desc
const pruneDesc = <T>(obj: T, visited = new Set<any>()): T => {
if (typeof obj !== "object" || obj === null) return obj;
if (visited.has(obj)) return obj;
visited.add(obj);
if (Array.isArray(obj)) {
return obj.map((item) => pruneDesc(item, visited)) as any;
}
const newObj: any = {};
for (const key in obj) {
if (key !== "desc") {
newObj[key] = pruneDesc(obj[key], visited);
}
}
return newObj;
};
return pruneDesc(m);
});
expect(sortMesssages(currentInfo.messages)).toEqual(
sortMesssages(expectedInfo.messages),
);
}
});
});
}
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment