perf(ci): scope Lastleg tests to affected changes

This commit is contained in:
2026-10-21 18:28:15 +08:00
parent 6d04659ced
commit 6088ff10c2
5 changed files with 419 additions and 1 deletions
+2
View File
@@ -447,6 +447,8 @@ Admin/Client HTTP 脚本的离线合同只验证脚本结构、参数和模块
## 后端分层检查
Jenkins 总控的测试范围入口为 `../deploy/scripts/ci-test-scope.mjs`,由 `TEST_SCOPE=AUTO|MODULE` 控制,不部署、不访问业务数据库。`plan` 使用带门禁通过标记且覆盖目标服务的成功构建基线与当前源码生成 Job 临时目录的 JSON,`checks` 执行受影响的既有架构合同,`args` 只输出精确测试类参数,`verify` 拒绝缺失、陈旧、失败或全部跳过的所选类报告。AUTO 当前仅优化已识别的单 Lastleg 局部变更,公共/未知/配置/SQL/多服务等保留完整回归;完整编译保留,真实环境 opt-in 不计通过,JVM 内存与隔离配置不放宽。定向列表在默认 Surefire execution 执行,关闭后置 SQL execution 的重复执行;未指定定向参数时原完整 SQL 回归不变。静态筛选边界及 MODULE 使用条件见工作区 `docs/测试环境Jenkins部署指南.md`;直接合同为 `ci-test-scope.test.mjs`。本地普通任务继续使用下述显式范围入口,不自动触发 CI 或增加完整回归。
`backend-check.ps1` 是本地日常入口,PowerShell 7 执行,自动经 `local-check.ps1` 占用一个重型名额。全量触发条件唯一见 `../AGENTS.md`;不自动根据 Git diff 猜测跨服务消费者,不自动缓存或跳过测试。
在后端根目录执行,`-Modules` 使用逗号分隔的当前根 POM 模块名:
+153
View File
@@ -0,0 +1,153 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync, mkdtempSync, writeFileSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { execFileSync } from 'node:child_process'
import { planTests, verifyReports } from '../deploy/scripts/ci-test-scope.mjs'
const module = 'lastleg-service'
function java(type, body = '', set = 'main') {
const name = type.split('.').at(-1)
const pkg = type.slice(0, -name.length - 1)
return { path: `${module}/src/${set}/java/com/baiyue/lastleg/${type.replaceAll('.', '/')}.java`,
source: `package com.baiyue.lastleg.${pkg}; class ${name} { ${body} }` }
}
const source = java('dispatch.DispatchService')
const fixture = java('dispatch.Fixture', 'DispatchService service;', 'test')
const direct = java('dispatch.DispatchTest', 'Fixture fixture; @Test void check() {}', 'test')
const consumer = java('delivery.DeliveryService', 'DispatchService service;')
const indirect = java('delivery.DeliveryTest', 'DeliveryService service; @Test void check() {}', 'test')
const unrelated = java('selfoperated.AnalyticsTest', '@Test void check() {}', 'test')
const security = java('workflow.OrderSecurityTest', '@Test void check() {}', 'test')
const sources = [source, fixture, direct, consumer, indirect, unrelated, security]
const plan = (options = {}) => planTests({ modules: [module], baseline: '1234567',
changes: [source.path], sources, ...options })
test('changed Java selects transitive callers and fixture consumers without unrelated domains', () => {
const result = plan()
assert.equal(result.scope, 'AFFECTED')
assert.deepEqual(result.tests, [
'com.baiyue.lastleg.delivery.DeliveryTest', 'com.baiyue.lastleg.dispatch.DispatchTest',
'com.baiyue.lastleg.workflow.OrderSecurityTest'
])
})
test('implementation changes reach callers of interfaces, including duplicate simple names', () => {
const contract = java('dispatch.Port')
const implementation = java('dispatch.Adapter', 'implements Port')
const portCaller = java('fleet.PortCaller', 'Port port;')
const portTest = java('fleet.PortCallerTest', 'PortCaller service; @Test void check() {}', 'test')
const result = plan({ changes: [implementation.path], sources: [...sources, contract, implementation, portCaller, portTest] })
assert.ok(result.tests.includes('com.baiyue.lastleg.fleet.PortCallerTest'))
})
test('SQL tables expand to other mappers and their tests', () => {
const mapper = java('dispatch.SourceMapper', 'String sql = "UPDATE ll_dispatch_source SET version=2";')
const sibling = java('delivery.DeliveryMapper', 'String sql = "UPDATE ll_dispatch_source SET version=1";')
const check = java('delivery.DeliveryMapperTest', 'DeliveryMapper mapper; @Test void check() {}', 'test')
const result = plan({ changes: [mapper.path], sources: [...sources, mapper, sibling, check] })
assert.ok(result.tests.includes('com.baiyue.lastleg.delivery.DeliveryMapperTest'))
})
test('unchanged writes beside a changed SELECT do not pull in unrelated table consumers', () => {
const mapper = java('dispatch.QueryMapper', '@Update("UPDATE ll_shared SET value=1") void write(); @Select("SELECT value FROM ll_source") void read();')
mapper.previousSource = mapper.source.replace('SELECT value', 'SELECT old_value')
const check = java('dispatch.QueryMapperTest', 'QueryMapper mapper; @Test void check() {}', 'test')
const unrelatedTable = java('selfoperated.SharedTableTest', 'String query = "SELECT * FROM ll_shared"; @Test void check() {}', 'test')
const result = plan({ changes: [mapper.path], sources: [...sources, mapper, check, unrelatedTable] })
assert.ok(result.tests.includes('com.baiyue.lastleg.dispatch.QueryMapperTest'))
assert.ok(!result.tests.includes('com.baiyue.lastleg.selfoperated.SharedTableTest'))
mapper.source = mapper.source.replace('value=1', 'value=2')
assert.ok(plan({ changes: [mapper.path], sources: [...sources, mapper, check, unrelatedTable] })
.tests.includes('com.baiyue.lastleg.selfoperated.SharedTableTest'))
})
test('environment opt-in classes are reported separately and do not count as passing tests', () => {
const optional = java('dispatch.RealMysqlTest', 'DispatchService service; @Test void check() {}', 'test')
optional.source = optional.source.replace('class RealMysqlTest', '@EnabledIfSystemProperty(named="live",matches="true") class RealMysqlTest')
const result = plan({ sources: [...sources, optional] })
assert.ok(!result.tests.includes('com.baiyue.lastleg.dispatch.RealMysqlTest'))
assert.deepEqual(result.optInTests, ['com.baiyue.lastleg.dispatch.RealMysqlTest'])
})
test('changed test fixtures and deleted Java types keep their dependent tests', () => {
assert.ok(plan({ changes: [fixture.path] }).tests.includes('com.baiyue.lastleg.dispatch.DispatchTest'))
const deleted = java('dispatch.RemovedService')
const user = java('delivery.ReplacementTest', 'String old = "RemovedService"; @Test void check() {}', 'test')
assert.ok(plan({ changes: [deleted.path], sources: [...sources, user] }).tests.includes('com.baiyue.lastleg.delivery.ReplacementTest'))
})
test('public API changes also select the existing contract tests', () => {
const contract = java('selfoperated.AnalyticsOpenApiContractTest', '@Test void check() {}', 'test')
const result = plan({ changes: [source.path, `${module}/src/main/resources/openapi/dispatch.yaml`], sources: [...sources, contract] })
assert.ok(result.tests.includes('com.baiyue.lastleg.selfoperated.AnalyticsOpenApiContractTest'))
})
test('shared, unknown, configuration, schema and missing-baseline changes retain full regression', () => {
for (const path of ['pom.xml', 'Jenkinsfile', 'common-core/src/main/java/Core.java',
'deploy/scripts/run-ci-maven.sh', 'config/nacos/test/lastleg.yml',
`${module}/pom.xml`, `${module}/src/main/resources/db/lastleg.sql`,
`${module}/src/test/resources/test-config.yml`, `${module}/src/main/java/com/baiyue/lastleg/entity/Order.java`]) {
assert.equal(plan({ changes: [source.path, path] }).scope, 'MODULE', path)
}
assert.equal(plan({ baseline: '' }).scope, 'MODULE')
assert.equal(plan({ mode: 'MODULE' }).scope, 'MODULE')
assert.equal(plan({ modules: ['order'] }).scope, 'MODULE')
assert.equal(plan({ modules: [module, 'iam'] }).scope, 'MODULE')
assert.equal(plan({ changes: [] }).scope, 'MODULE')
assert.equal(plan({ sources: [source] }).scope, 'MODULE')
assert.throws(() => plan({ mode: 'SKIP' }))
})
test('architecture script changes run their own checks instead of broadening Java regression', () => {
const result = plan({ changes: [source.path, 'scripts/agents-index-contract.ps1',
`${module}/scripts/analyze-lastleg-dependencies.test.mjs`] })
assert.equal(result.scope, 'AFFECTED')
assert.deepEqual(result.staticChecks, [`${module}/scripts/analyze-lastleg-dependencies.test.mjs`])
})
test('focused report verification rejects missing, failed and entirely skipped classes', () => {
const focused = { scope: 'AFFECTED', tests: ['com.example.Test'] }
const report = { name: 'com.example.Test', tests: 2, skipped: 0, failures: 0, errors: 0 }
verifyReports(focused, [report])
verifyReports(focused, [{ ...report, name: 'com.example.Test$Nested' }])
for (const reports of [[], [{ ...report, tests: 0 }], [{ ...report, skipped: 2 }],
[{ ...report, failures: 1 }], [{ ...report, errors: 1 }], [{ ...report, tests: NaN }]]) {
assert.throws(() => verifyReports(focused, reports))
}
})
test('CLI prevents double execution of selected classes in the SQL semantic execution', () => {
const directory = mkdtempSync(join(tmpdir(), 'ci-test-scope-'))
try {
const file = join(directory, 'plan.json')
writeFileSync(file, JSON.stringify(plan()))
const args = execFileSync(process.execPath,
['deploy/scripts/ci-test-scope.mjs', 'args', '--plan', file], { encoding: 'utf8' })
assert.match(args, /-Dtest=com\.baiyue/)
assert.match(args, /-Dsurefire.failIfNoSpecifiedTests=false/)
assert.match(args, /-Dci.sql-semantic.skip=true/)
const pom = readFileSync(new URL('../lastleg-service/pom.xml', import.meta.url), 'utf8')
assert.match(pom, /<ci.sql-semantic.skip>false<\/ci.sql-semantic.skip>/)
const execution = pom.slice(pom.indexOf('<id>sql-semantic-tests</id>'))
assert.match(execution, /<skip>\$\{ci.sql-semantic.skip\}<\/skip>/)
assert.match(execution, /<reuseForks>false<\/reuseForks>/)
} finally { rmSync(directory, { recursive: true, force: true }) }
})
test('Jenkins plans from a successful baseline, verifies fresh reports and preserves module override', () => {
const pipeline = readFileSync(new URL('../Jenkinsfile', import.meta.url), 'utf8')
assert.match(pipeline, /candidate.result == 'SUCCESS' && markerIndex >= 0/)
assert.match(pipeline, /requiredServices.every \{ metadata\[1\].split\(','\).contains\(it\) \}/)
assert.match(pipeline, /if \(env.TEST_GATE_PASSED == 'true'\)/)
assert.match(pipeline, /TESTS=PASSED \$\{env.SOURCE_COMMIT\} \$\{env.SELECTED_SERVICES\}/)
assert.match(pipeline, /summary.indexOf\(' \| '\)/)
assert.match(pipeline, /env.TEST_BASE_COMMIT = previousSuccessfulTestCommit/)
assert.match(pipeline, /choice\(name: 'TEST_SCOPE', choices: \['AUTO', 'MODULE'\]/)
assert.match(pipeline, /ci-test-scope.mjs plan --modules/)
assert.match(pipeline, /ci-test-scope.mjs checks --plan/)
assert.match(pipeline, /-am test \\\$test_arguments/)
assert.match(pipeline, /ci-test-scope.mjs verify --plan/)
assert.match(pipeline, /booleanParam\(name: 'SKIP_TESTS', value: true\)/)
})