feat(perf): 增加大数据压测脚手架
This commit is contained in:
75
docs/performance.md
Normal file
75
docs/performance.md
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
# V2.6 Performance Harness
|
||||||
|
|
||||||
|
V2.6 adds a deterministic large-data fixture and a small HTTP performance harness for the current hot paths. The goal is to make performance regressions visible before adding more background jobs and Xiaobao automation.
|
||||||
|
|
||||||
|
## Fixture Sizes
|
||||||
|
|
||||||
|
The fixture script creates only `perf-*` rows and can be rerun safely. It covers:
|
||||||
|
|
||||||
|
- products
|
||||||
|
- projects
|
||||||
|
- versions
|
||||||
|
- requirements
|
||||||
|
- version plans
|
||||||
|
- dev tasks
|
||||||
|
- test cases
|
||||||
|
- bugs
|
||||||
|
- work activities
|
||||||
|
- Xiaobao risk summaries
|
||||||
|
|
||||||
|
Preset sizes:
|
||||||
|
|
||||||
|
| Size | Purpose |
|
||||||
|
| --- | --- |
|
||||||
|
| `small` | Local smoke fixture. Fast dry-run and minimal database seed. |
|
||||||
|
| `medium` | Default performance gate for V2.6 hot APIs. |
|
||||||
|
| `large` | Stress fixture for query/index audit work. |
|
||||||
|
|
||||||
|
Stable anchors used by the harness:
|
||||||
|
|
||||||
|
```text
|
||||||
|
productId=perf-product-001
|
||||||
|
versionId=perf-version-001-001-001
|
||||||
|
userId=perf-user-dev-01
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
Dry-run without database access:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node scripts/seed-large-dataset.mjs --size small --dry-run
|
||||||
|
node scripts/perf-check.mjs --base-url http://localhost:3001/api/v1 --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
Seed a database:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm perf:seed -- --size small
|
||||||
|
```
|
||||||
|
|
||||||
|
Run the hot-path harness against a running NestJS API:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm perf:check -- --base-url http://localhost:3001/api/v1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Hot Probes
|
||||||
|
|
||||||
|
`perf-check` measures p50 and p95 latency, records HTTP status code counts, and exits non-zero when a request fails or p95 exceeds the current query budget.
|
||||||
|
|
||||||
|
| Probe | Endpoint | p95 Budget |
|
||||||
|
| --- | --- | ---: |
|
||||||
|
| Runtime version | `/health/version` | 500ms |
|
||||||
|
| Requirement pool | `/v2.2/requirements?productId=perf-product-001&q=REQ&limit=50` | 1000ms |
|
||||||
|
| Version detail | `/v2.2/versions/perf-version-001-001-001/detail-data` | 1500ms |
|
||||||
|
| Workspace | `/v2.2/workspace?userId=perf-user-dev-01` | 1200ms |
|
||||||
|
| Xiaobao warning | `/v2.2/xiaobao-warning?manager=true` | 1000ms |
|
||||||
|
|
||||||
|
The medium fixture is the V2.6 acceptance target. Large fixture runs are for index audit and explain-plan work, not for every local commit.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- `seed-large-dataset` uses deterministic IDs and dates so repeated runs are comparable.
|
||||||
|
- Non-dry-run seeding deletes and recreates only `perf-*` rows.
|
||||||
|
- The harness intentionally depends on public API endpoints instead of calling Prisma directly; it measures the same path the frontend uses.
|
||||||
@@ -7,6 +7,8 @@
|
|||||||
"lint": "turbo lint",
|
"lint": "turbo lint",
|
||||||
"type-check": "turbo type-check",
|
"type-check": "turbo type-check",
|
||||||
"test": "turbo test",
|
"test": "turbo test",
|
||||||
|
"perf:seed": "node scripts/seed-large-dataset.mjs",
|
||||||
|
"perf:check": "node scripts/perf-check.mjs",
|
||||||
"deploy:verify": "node scripts/verify-production-deploy.mjs",
|
"deploy:verify": "node scripts/verify-production-deploy.mjs",
|
||||||
"deploy:check-runtime": "node scripts/check-runtime-version.mjs",
|
"deploy:check-runtime": "node scripts/check-runtime-version.mjs",
|
||||||
"deploy:local:build": "docker compose --env-file .env.local-server -f docker-compose.local.yml build",
|
"deploy:local:build": "docker compose --env-file .env.local-server -f docker-compose.local.yml build",
|
||||||
|
|||||||
251
scripts/perf-check.mjs
Normal file
251
scripts/perf-check.mjs
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import { performance } from 'node:perf_hooks';
|
||||||
|
|
||||||
|
const DEFAULT_THRESHOLDS = {
|
||||||
|
healthVersion: 500,
|
||||||
|
requirementPool: 1000,
|
||||||
|
versionDetail: 1500,
|
||||||
|
workspace: 1200,
|
||||||
|
xiaobaoWarning: 1000,
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULTS = {
|
||||||
|
baseUrl: 'http://localhost:3001/api/v1',
|
||||||
|
productId: 'perf-product-001',
|
||||||
|
versionId: 'perf-version-001-001-001',
|
||||||
|
userId: 'perf-user-dev-01',
|
||||||
|
iterations: 5,
|
||||||
|
warmup: 1,
|
||||||
|
dryRun: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
function parseArgs(argv) {
|
||||||
|
const args = { ...DEFAULTS };
|
||||||
|
for (let index = 0; index < argv.length; index += 1) {
|
||||||
|
const arg = argv[index];
|
||||||
|
if (arg === '--dry-run') {
|
||||||
|
args.dryRun = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (arg === '--base-url') {
|
||||||
|
args.baseUrl = argv[++index] ?? args.baseUrl;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (arg.startsWith('--base-url=')) {
|
||||||
|
args.baseUrl = arg.slice('--base-url='.length);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (arg === '--product-id') {
|
||||||
|
args.productId = argv[++index] ?? args.productId;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (arg === '--version-id') {
|
||||||
|
args.versionId = argv[++index] ?? args.versionId;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (arg === '--user-id') {
|
||||||
|
args.userId = argv[++index] ?? args.userId;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (arg === '--iterations') {
|
||||||
|
args.iterations = parsePositiveInt(argv[++index], args.iterations);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (arg === '--warmup') {
|
||||||
|
args.warmup = parsePositiveInt(argv[++index], args.warmup, 0);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (arg === '--help' || arg === '-h') {
|
||||||
|
printHelp();
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
throw new Error(`Unknown argument: ${arg}`);
|
||||||
|
}
|
||||||
|
args.baseUrl = args.baseUrl.replace(/\/+$/, '');
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
function printHelp() {
|
||||||
|
console.log(`Usage: node scripts/perf-check.mjs [--base-url URL] [--dry-run]
|
||||||
|
|
||||||
|
Runs the V2.6 hot API performance harness.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--base-url API root. Defaults to ${DEFAULTS.baseUrl}
|
||||||
|
--product-id Requirement pool product id. Defaults to ${DEFAULTS.productId}
|
||||||
|
--version-id Version detail id. Defaults to ${DEFAULTS.versionId}
|
||||||
|
--user-id Workspace user id. Defaults to ${DEFAULTS.userId}
|
||||||
|
--iterations Timed iterations per probe. Defaults to ${DEFAULTS.iterations}
|
||||||
|
--warmup Warmup iterations per probe. Defaults to ${DEFAULTS.warmup}
|
||||||
|
--dry-run Print probes without making HTTP requests.
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parsePositiveInt(raw, fallback, min = 1) {
|
||||||
|
const parsed = Number(raw);
|
||||||
|
if (!Number.isFinite(parsed)) return fallback;
|
||||||
|
return Math.max(min, Math.floor(parsed));
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildProbes(args) {
|
||||||
|
const requirementQuery = new URLSearchParams({
|
||||||
|
productId: args.productId,
|
||||||
|
q: 'REQ',
|
||||||
|
limit: '50',
|
||||||
|
});
|
||||||
|
const workspaceQuery = new URLSearchParams({ userId: args.userId });
|
||||||
|
const xiaobaoQuery = new URLSearchParams({ manager: 'true' });
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: 'healthVersion',
|
||||||
|
label: 'health/version',
|
||||||
|
method: 'GET',
|
||||||
|
path: '/health/version',
|
||||||
|
thresholdMs: DEFAULT_THRESHOLDS.healthVersion,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'requirementPool',
|
||||||
|
label: 'v2.2 requirement query',
|
||||||
|
method: 'GET',
|
||||||
|
path: `/v2.2/requirements?${requirementQuery.toString()}`,
|
||||||
|
thresholdMs: DEFAULT_THRESHOLDS.requirementPool,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'versionDetail',
|
||||||
|
label: 'v2.2 version detail',
|
||||||
|
method: 'GET',
|
||||||
|
path: `/v2.2/versions/${encodeURIComponent(args.versionId)}/detail-data`,
|
||||||
|
thresholdMs: DEFAULT_THRESHOLDS.versionDetail,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'workspace',
|
||||||
|
label: 'v2.2 workspace',
|
||||||
|
method: 'GET',
|
||||||
|
path: `/v2.2/workspace?${workspaceQuery.toString()}`,
|
||||||
|
thresholdMs: DEFAULT_THRESHOLDS.workspace,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'xiaobaoWarning',
|
||||||
|
label: 'v2.2 xiaobao warning',
|
||||||
|
method: 'GET',
|
||||||
|
path: `/v2.2/xiaobao-warning?${xiaobaoQuery.toString()}`,
|
||||||
|
thresholdMs: DEFAULT_THRESHOLDS.xiaobaoWarning,
|
||||||
|
},
|
||||||
|
].map((probe) => ({
|
||||||
|
...probe,
|
||||||
|
url: `${args.baseUrl}${probe.path}`,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function measureProbe(probe, args) {
|
||||||
|
for (let index = 0; index < args.warmup; index += 1) {
|
||||||
|
await requestOnce(probe);
|
||||||
|
}
|
||||||
|
|
||||||
|
const samples = [];
|
||||||
|
const statuses = new Map();
|
||||||
|
const errors = [];
|
||||||
|
for (let index = 0; index < args.iterations; index += 1) {
|
||||||
|
const result = await requestOnce(probe);
|
||||||
|
samples.push(result.durationMs);
|
||||||
|
statuses.set(result.status, (statuses.get(result.status) ?? 0) + 1);
|
||||||
|
if (result.error) errors.push(result.error);
|
||||||
|
if (result.status < 200 || result.status >= 300) {
|
||||||
|
errors.push(`HTTP ${result.status}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const p50 = percentile(samples, 50);
|
||||||
|
const p95 = percentile(samples, 95);
|
||||||
|
if (p95 > probe.thresholdMs) {
|
||||||
|
errors.push(`p95 ${p95.toFixed(1)}ms > budget ${probe.thresholdMs}ms`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...probe,
|
||||||
|
count: samples.length,
|
||||||
|
p50,
|
||||||
|
p95,
|
||||||
|
statuses: [...statuses.entries()].sort(([a], [b]) => a - b),
|
||||||
|
ok: errors.length === 0,
|
||||||
|
errors,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function requestOnce(probe) {
|
||||||
|
const startedAt = performance.now();
|
||||||
|
try {
|
||||||
|
const response = await fetch(probe.url, { method: probe.method });
|
||||||
|
await response.arrayBuffer();
|
||||||
|
return {
|
||||||
|
status: response.status,
|
||||||
|
durationMs: performance.now() - startedAt,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
status: 0,
|
||||||
|
durationMs: performance.now() - startedAt,
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function percentile(values, pct) {
|
||||||
|
if (values.length === 0) return 0;
|
||||||
|
const sorted = [...values].sort((a, b) => a - b);
|
||||||
|
const index = Math.ceil((pct / 100) * sorted.length) - 1;
|
||||||
|
return sorted[Math.max(0, Math.min(sorted.length - 1, index))];
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatStatuses(statuses) {
|
||||||
|
return statuses.map(([status, count]) => `${status}:${count}`).join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function printProbePlan(probes) {
|
||||||
|
console.log('Performance harness dry-run');
|
||||||
|
for (const probe of probes) {
|
||||||
|
console.log(`${probe.key}\t${probe.method}\t${probe.url}\tbudget_p95_ms=${probe.thresholdMs}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function printResults(results) {
|
||||||
|
console.log('probe\tstatus\tp50_ms\tp95_ms\tbudget_p95_ms\tok');
|
||||||
|
for (const result of results) {
|
||||||
|
console.log([
|
||||||
|
result.key,
|
||||||
|
formatStatuses(result.statuses),
|
||||||
|
result.p50.toFixed(1),
|
||||||
|
result.p95.toFixed(1),
|
||||||
|
result.thresholdMs,
|
||||||
|
result.ok ? 'yes' : 'no',
|
||||||
|
].join('\t'));
|
||||||
|
if (result.errors.length > 0) {
|
||||||
|
for (const error of result.errors) console.error(`${result.key}: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const args = parseArgs(process.argv.slice(2));
|
||||||
|
const probes = buildProbes(args);
|
||||||
|
if (args.dryRun) {
|
||||||
|
printProbePlan(probes);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = [];
|
||||||
|
for (const probe of probes) {
|
||||||
|
results.push(await measureProbe(probe, args));
|
||||||
|
}
|
||||||
|
printResults(results);
|
||||||
|
if (results.some((result) => !result.ok)) {
|
||||||
|
process.exitCode = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error(error instanceof Error ? error.message : error);
|
||||||
|
process.exitCode = 1;
|
||||||
|
});
|
||||||
|
|
||||||
661
scripts/seed-large-dataset.mjs
Normal file
661
scripts/seed-large-dataset.mjs
Normal file
@@ -0,0 +1,661 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import { createRequire } from 'node:module';
|
||||||
|
import { mkdir, writeFile } from 'node:fs/promises';
|
||||||
|
import { dirname, resolve } from 'node:path';
|
||||||
|
|
||||||
|
const FIXTURE_VERSION = 'v2.6-large-data-fixture-1';
|
||||||
|
const ID_PREFIX = 'perf-';
|
||||||
|
const BASE_DATE = new Date('2026-07-01T09:00:00.000Z');
|
||||||
|
const OUTPUT_PATH = '.tmp/perf-fixture.json';
|
||||||
|
|
||||||
|
const SIZE_PRESETS = {
|
||||||
|
small: {
|
||||||
|
products: 1,
|
||||||
|
projectsPerProduct: 2,
|
||||||
|
versionsPerProject: 2,
|
||||||
|
requirementsPerVersion: 20,
|
||||||
|
looseRequirementsPerProduct: 20,
|
||||||
|
plansPerVersion: 3,
|
||||||
|
devTasksPerRequirement: 2,
|
||||||
|
testCasesPerRequirement: 2,
|
||||||
|
bugsPerVersion: 12,
|
||||||
|
activitiesPerVersion: 16,
|
||||||
|
users: 8,
|
||||||
|
},
|
||||||
|
medium: {
|
||||||
|
products: 2,
|
||||||
|
projectsPerProduct: 8,
|
||||||
|
versionsPerProject: 4,
|
||||||
|
requirementsPerVersion: 80,
|
||||||
|
looseRequirementsPerProduct: 300,
|
||||||
|
plansPerVersion: 3,
|
||||||
|
devTasksPerRequirement: 3,
|
||||||
|
testCasesPerRequirement: 3,
|
||||||
|
bugsPerVersion: 80,
|
||||||
|
activitiesPerVersion: 120,
|
||||||
|
users: 40,
|
||||||
|
},
|
||||||
|
large: {
|
||||||
|
products: 4,
|
||||||
|
projectsPerProduct: 20,
|
||||||
|
versionsPerProject: 6,
|
||||||
|
requirementsPerVersion: 150,
|
||||||
|
looseRequirementsPerProduct: 2000,
|
||||||
|
plansPerVersion: 3,
|
||||||
|
devTasksPerRequirement: 4,
|
||||||
|
testCasesPerRequirement: 4,
|
||||||
|
bugsPerVersion: 180,
|
||||||
|
activitiesPerVersion: 300,
|
||||||
|
users: 120,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEV_STATUSES = ['todo', 'in_progress', 'testing', 'submitted'];
|
||||||
|
const TEST_STATUSES = ['pending', 'running', 'passed', 'failed', 'blocked'];
|
||||||
|
const BUG_STATUSES = ['open', 'fixing', 'fixed', 'verifying', 'closed'];
|
||||||
|
const BUG_SEVERITIES = ['normal', 'major', 'critical'];
|
||||||
|
const PLAN_TYPES = ['research', 'product', 'ui'];
|
||||||
|
const REQUIREMENT_STATUSES = ['adopted', 'planned', 'developing', 'testing', 'released'];
|
||||||
|
|
||||||
|
function parseArgs(argv) {
|
||||||
|
const args = {
|
||||||
|
size: 'small',
|
||||||
|
dryRun: false,
|
||||||
|
json: false,
|
||||||
|
output: OUTPUT_PATH,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let index = 0; index < argv.length; index += 1) {
|
||||||
|
const arg = argv[index];
|
||||||
|
if (arg === '--dry-run') {
|
||||||
|
args.dryRun = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (arg === '--json') {
|
||||||
|
args.json = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (arg === '--size') {
|
||||||
|
args.size = argv[++index] ?? args.size;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (arg.startsWith('--size=')) {
|
||||||
|
args.size = arg.slice('--size='.length);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (arg === '--output') {
|
||||||
|
args.output = argv[++index] ?? args.output;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (arg.startsWith('--output=')) {
|
||||||
|
args.output = arg.slice('--output='.length);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (arg === '--help' || arg === '-h') {
|
||||||
|
printHelp();
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
throw new Error(`Unknown argument: ${arg}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!SIZE_PRESETS[args.size]) {
|
||||||
|
throw new Error(`Unsupported size "${args.size}". Use one of: ${Object.keys(SIZE_PRESETS).join(', ')}`);
|
||||||
|
}
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
function printHelp() {
|
||||||
|
console.log(`Usage: node scripts/seed-large-dataset.mjs [--size small|medium|large] [--dry-run] [--json]
|
||||||
|
|
||||||
|
Creates deterministic V2.6 performance fixture data using perf-* ids.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--size Fixture size. Defaults to small.
|
||||||
|
--dry-run Build and summarize rows without writing PostgreSQL.
|
||||||
|
--json Print the summary as JSON.
|
||||||
|
--output Metadata path written after a real seed. Defaults to ${OUTPUT_PATH}.
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pad(value, width = 3) {
|
||||||
|
return String(value).padStart(width, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
function hoursAfter(hours) {
|
||||||
|
return new Date(BASE_DATE.getTime() + hours * 60 * 60 * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function daysAfter(days) {
|
||||||
|
return hoursAfter(days * 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
function priority(index) {
|
||||||
|
return index % 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildFixture(sizeName) {
|
||||||
|
const preset = SIZE_PRESETS[sizeName];
|
||||||
|
const users = buildUsers(preset.users);
|
||||||
|
const products = [];
|
||||||
|
const projects = [];
|
||||||
|
const versions = [];
|
||||||
|
const requirements = [];
|
||||||
|
const versionPlans = [];
|
||||||
|
const devTasks = [];
|
||||||
|
const testCases = [];
|
||||||
|
const bugs = [];
|
||||||
|
const workActivities = [];
|
||||||
|
const xiaobaoRiskSummaries = [];
|
||||||
|
|
||||||
|
for (let productNo = 1; productNo <= preset.products; productNo += 1) {
|
||||||
|
const productId = `${ID_PREFIX}product-${pad(productNo)}`;
|
||||||
|
products.push({
|
||||||
|
id: productId,
|
||||||
|
name: `Performance Product ${pad(productNo)}`,
|
||||||
|
description: `Deterministic ${sizeName} fixture product ${productNo}`,
|
||||||
|
createdAt: daysAfter(productNo),
|
||||||
|
updatedAt: daysAfter(productNo),
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let looseNo = 1; looseNo <= preset.looseRequirementsPerProduct; looseNo += 1) {
|
||||||
|
requirements.push(buildRequirement({
|
||||||
|
productId,
|
||||||
|
projectId: null,
|
||||||
|
versionId: null,
|
||||||
|
productNo,
|
||||||
|
projectNo: 0,
|
||||||
|
versionNo: 0,
|
||||||
|
requirementNo: looseNo,
|
||||||
|
loose: true,
|
||||||
|
creatorId: users[looseNo % users.length].id,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let projectNo = 1; projectNo <= preset.projectsPerProduct; projectNo += 1) {
|
||||||
|
const projectId = `${ID_PREFIX}project-${pad(productNo)}-${pad(projectNo)}`;
|
||||||
|
projects.push({
|
||||||
|
id: projectId,
|
||||||
|
productId,
|
||||||
|
name: `Performance Project ${pad(productNo)}-${pad(projectNo)}`,
|
||||||
|
description: `Hot-path project ${projectNo}`,
|
||||||
|
createdAt: daysAfter(projectNo),
|
||||||
|
updatedAt: daysAfter(projectNo),
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let versionNo = 1; versionNo <= preset.versionsPerProject; versionNo += 1) {
|
||||||
|
const versionId = `${ID_PREFIX}version-${pad(productNo)}-${pad(projectNo)}-${pad(versionNo)}`;
|
||||||
|
const version = buildVersion({
|
||||||
|
productId,
|
||||||
|
projectId,
|
||||||
|
versionId,
|
||||||
|
productNo,
|
||||||
|
projectNo,
|
||||||
|
versionNo,
|
||||||
|
users,
|
||||||
|
});
|
||||||
|
versions.push(version);
|
||||||
|
|
||||||
|
for (let planNo = 1; planNo <= preset.plansPerVersion; planNo += 1) {
|
||||||
|
versionPlans.push(buildVersionPlan({
|
||||||
|
productId,
|
||||||
|
projectId,
|
||||||
|
versionId,
|
||||||
|
planNo,
|
||||||
|
ownerId: users[(planNo + versionNo) % users.length].id,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const versionRequirementIds = [];
|
||||||
|
for (let requirementNo = 1; requirementNo <= preset.requirementsPerVersion; requirementNo += 1) {
|
||||||
|
const req = buildRequirement({
|
||||||
|
productId,
|
||||||
|
projectId,
|
||||||
|
versionId,
|
||||||
|
productNo,
|
||||||
|
projectNo,
|
||||||
|
versionNo,
|
||||||
|
requirementNo,
|
||||||
|
loose: false,
|
||||||
|
creatorId: users[requirementNo % users.length].id,
|
||||||
|
});
|
||||||
|
requirements.push(req);
|
||||||
|
versionRequirementIds.push(req.id);
|
||||||
|
|
||||||
|
for (let taskNo = 1; taskNo <= preset.devTasksPerRequirement; taskNo += 1) {
|
||||||
|
devTasks.push(buildDevTask({
|
||||||
|
productId,
|
||||||
|
projectId,
|
||||||
|
versionId,
|
||||||
|
requirementId: req.id,
|
||||||
|
requirementProductId: productId,
|
||||||
|
requirementNo,
|
||||||
|
taskNo,
|
||||||
|
assigneeId: users[(requirementNo + taskNo) % users.length].id,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let caseNo = 1; caseNo <= preset.testCasesPerRequirement; caseNo += 1) {
|
||||||
|
testCases.push(buildTestCase({
|
||||||
|
productId,
|
||||||
|
projectId,
|
||||||
|
versionId,
|
||||||
|
requirementId: req.id,
|
||||||
|
requirementProductId: productId,
|
||||||
|
requirementNo,
|
||||||
|
caseNo,
|
||||||
|
assigneeId: users[(requirementNo + caseNo + 2) % users.length].id,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let bugNo = 1; bugNo <= preset.bugsPerVersion; bugNo += 1) {
|
||||||
|
bugs.push(buildBug({
|
||||||
|
productId,
|
||||||
|
projectId,
|
||||||
|
versionId,
|
||||||
|
bugNo,
|
||||||
|
assigneeId: users[(bugNo + versionNo) % users.length].id,
|
||||||
|
testCaseId: testCases.find((row) => row.versionId === versionId)?.id,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let activityNo = 1; activityNo <= preset.activitiesPerVersion; activityNo += 1) {
|
||||||
|
const source = devTasks.find((row) => row.versionId === versionId && row.assigneeId === users[activityNo % users.length].id)
|
||||||
|
?? devTasks.find((row) => row.versionId === versionId);
|
||||||
|
workActivities.push(buildWorkActivity({
|
||||||
|
productId,
|
||||||
|
projectId,
|
||||||
|
versionId,
|
||||||
|
activityNo,
|
||||||
|
actor: users[activityNo % users.length],
|
||||||
|
source,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
xiaobaoRiskSummaries.push(buildXiaobaoSummary({
|
||||||
|
version,
|
||||||
|
bugCount: preset.bugsPerVersion,
|
||||||
|
unfinishedCount: versionRequirementIds.length,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
size: sizeName,
|
||||||
|
anchors: {
|
||||||
|
productId: `${ID_PREFIX}product-001`,
|
||||||
|
projectId: `${ID_PREFIX}project-001-001`,
|
||||||
|
versionId: `${ID_PREFIX}version-001-001-001`,
|
||||||
|
userId: `${ID_PREFIX}user-dev-01`,
|
||||||
|
},
|
||||||
|
rows: {
|
||||||
|
users,
|
||||||
|
products,
|
||||||
|
projects,
|
||||||
|
versions,
|
||||||
|
requirements,
|
||||||
|
versionPlans,
|
||||||
|
devTasks,
|
||||||
|
testCases,
|
||||||
|
bugs,
|
||||||
|
workActivities,
|
||||||
|
xiaobaoRiskSummaries,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildUsers(count) {
|
||||||
|
return Array.from({ length: count }, (_, index) => {
|
||||||
|
const no = index + 1;
|
||||||
|
return {
|
||||||
|
id: `${ID_PREFIX}user-dev-${pad(no, 2)}`,
|
||||||
|
email: `perf-user-${pad(no, 2)}@example.test`,
|
||||||
|
name: `Perf User ${pad(no, 2)}`,
|
||||||
|
username: `perf_user_${pad(no, 2)}`,
|
||||||
|
departmentId: no % 3 === 0 ? 'dept-2-3' : no % 2 === 0 ? 'dept-2-2' : 'dept-2-1',
|
||||||
|
roleId: no % 3 === 0 ? 'role-test' : 'role-dev',
|
||||||
|
phone: '',
|
||||||
|
password: 'Perf@2026',
|
||||||
|
isSystem: false,
|
||||||
|
createdAt: BASE_DATE,
|
||||||
|
updatedAt: BASE_DATE,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildVersion(input) {
|
||||||
|
const releaseDate = daysAfter(20 + input.versionNo);
|
||||||
|
return {
|
||||||
|
id: input.versionId,
|
||||||
|
productId: input.productId,
|
||||||
|
projectId: input.projectId,
|
||||||
|
name: `V${input.productNo}.${input.projectNo}.${input.versionNo}`,
|
||||||
|
description: 'Performance hot-path version',
|
||||||
|
status: input.versionNo % 4 === 0 ? 'paused' : 'developing',
|
||||||
|
currentStage: input.versionNo % 2 === 0 ? 'testing' : 'development',
|
||||||
|
startDate: daysAfter(input.versionNo),
|
||||||
|
expectedReleaseDate: releaseDate,
|
||||||
|
releaseDate,
|
||||||
|
members: input.users.slice(0, Math.min(6, input.users.length)).map((user, index) => ({
|
||||||
|
id: user.id,
|
||||||
|
name: user.name,
|
||||||
|
role: index % 3 === 0 ? 'testing' : index % 2 === 0 ? 'backend' : 'frontend',
|
||||||
|
})),
|
||||||
|
progress: [],
|
||||||
|
priority: priority(input.versionNo),
|
||||||
|
links: {},
|
||||||
|
createdAt: daysAfter(input.versionNo),
|
||||||
|
updatedAt: daysAfter(input.versionNo + 1),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRequirement(input) {
|
||||||
|
const scoped = input.loose
|
||||||
|
? `LOOSE-${pad(input.productNo)}-${pad(input.requirementNo, 5)}`
|
||||||
|
: `${pad(input.productNo)}-${pad(input.projectNo)}-${pad(input.versionNo)}-${pad(input.requirementNo, 5)}`;
|
||||||
|
return {
|
||||||
|
id: `${ID_PREFIX}req-${scoped}`,
|
||||||
|
productId: input.productId,
|
||||||
|
projectId: input.projectId,
|
||||||
|
versionId: input.versionId,
|
||||||
|
code: `REQ-${scoped}`,
|
||||||
|
title: `Requirement ${scoped}`,
|
||||||
|
description: `Deterministic requirement ${scoped}`,
|
||||||
|
status: input.loose ? 'adopted' : REQUIREMENT_STATUSES[input.requirementNo % REQUIREMENT_STATUSES.length],
|
||||||
|
priority: priority(input.requirementNo),
|
||||||
|
type: input.requirementNo % 2 === 0 ? 'feature' : 'optimization',
|
||||||
|
sourceType: input.requirementNo % 3 === 0 ? 'customer' : 'internal',
|
||||||
|
sourceTarget: input.requirementNo % 3 === 0 ? 'perf-customer' : 'perf-team',
|
||||||
|
platform: input.requirementNo % 2 === 0 ? 'web,ios' : 'web',
|
||||||
|
creatorId: input.creatorId,
|
||||||
|
createdAt: hoursAfter(input.requirementNo),
|
||||||
|
updatedAt: hoursAfter(input.requirementNo + 1),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildVersionPlan(input) {
|
||||||
|
const type = PLAN_TYPES[(input.planNo - 1) % PLAN_TYPES.length];
|
||||||
|
return {
|
||||||
|
id: `${ID_PREFIX}plan-${input.versionId}-${type}`,
|
||||||
|
versionId: input.versionId,
|
||||||
|
productId: input.productId,
|
||||||
|
projectId: input.projectId,
|
||||||
|
type,
|
||||||
|
title: `${type} plan for ${input.versionId}`,
|
||||||
|
status: input.planNo === 1 ? 'completed' : 'in_progress',
|
||||||
|
ownerId: input.ownerId,
|
||||||
|
expectedStartAt: daysAfter(input.planNo),
|
||||||
|
expectedEndAt: daysAfter(input.planNo + 3),
|
||||||
|
actualStartAt: daysAfter(input.planNo),
|
||||||
|
completedAt: input.planNo === 1 ? daysAfter(input.planNo + 2) : null,
|
||||||
|
resultUrl: input.planNo === 1 ? 'https://example.test/prototype' : null,
|
||||||
|
requirementCoverage: [],
|
||||||
|
logs: [],
|
||||||
|
createdAt: daysAfter(input.planNo),
|
||||||
|
updatedAt: daysAfter(input.planNo + 1),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDevTask(input) {
|
||||||
|
const status = DEV_STATUSES[(input.requirementNo + input.taskNo) % DEV_STATUSES.length];
|
||||||
|
const blocked = status !== 'submitted' && input.taskNo % 11 === 0;
|
||||||
|
return {
|
||||||
|
id: `${ID_PREFIX}dev-${input.versionId}-${pad(input.requirementNo, 5)}-${pad(input.taskNo, 2)}`,
|
||||||
|
versionId: input.versionId,
|
||||||
|
productId: input.productId,
|
||||||
|
projectId: input.projectId,
|
||||||
|
requirementId: input.requirementId,
|
||||||
|
requirementProductId: input.requirementProductId,
|
||||||
|
categoryId: null,
|
||||||
|
code: `DEV-${pad(input.requirementNo, 5)}-${pad(input.taskNo, 2)}`,
|
||||||
|
title: `Dev task ${pad(input.requirementNo, 5)}-${pad(input.taskNo, 2)}`,
|
||||||
|
description: 'Performance fixture development task',
|
||||||
|
status,
|
||||||
|
priority: priority(input.requirementNo + input.taskNo),
|
||||||
|
assigneeId: input.assigneeId,
|
||||||
|
creatorId: input.assigneeId,
|
||||||
|
isBlocked: blocked,
|
||||||
|
blockReason: blocked ? 'Fixture blocker' : null,
|
||||||
|
expectedStartAt: daysAfter(input.taskNo),
|
||||||
|
expectedEndAt: daysAfter(input.taskNo + 2),
|
||||||
|
startDate: status === 'todo' ? null : daysAfter(input.taskNo),
|
||||||
|
completedAt: status === 'submitted' ? daysAfter(input.taskNo + 2) : null,
|
||||||
|
estimateHours: 4 + (input.taskNo % 5),
|
||||||
|
aiEstimateHours: 3 + (input.taskNo % 4),
|
||||||
|
references: [{ type: 'requirement', id: input.requirementId }],
|
||||||
|
aiDraft: false,
|
||||||
|
aiDraftAt: null,
|
||||||
|
createdAt: hoursAfter(input.requirementNo + input.taskNo),
|
||||||
|
updatedAt: hoursAfter(input.requirementNo + input.taskNo + 1),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTestCase(input) {
|
||||||
|
const status = TEST_STATUSES[(input.requirementNo + input.caseNo) % TEST_STATUSES.length];
|
||||||
|
return {
|
||||||
|
id: `${ID_PREFIX}case-${input.versionId}-${pad(input.requirementNo, 5)}-${pad(input.caseNo, 2)}`,
|
||||||
|
versionId: input.versionId,
|
||||||
|
productId: input.productId,
|
||||||
|
projectId: input.projectId,
|
||||||
|
requirementId: input.requirementId,
|
||||||
|
requirementProductId: input.requirementProductId,
|
||||||
|
categoryId: null,
|
||||||
|
code: `TC-${pad(input.requirementNo, 5)}-${pad(input.caseNo, 2)}`,
|
||||||
|
title: `Test case ${pad(input.requirementNo, 5)}-${pad(input.caseNo, 2)}`,
|
||||||
|
description: 'Performance fixture test case',
|
||||||
|
status,
|
||||||
|
roundNo: input.caseNo % 3 === 0 ? 2 : 1,
|
||||||
|
priority: priority(input.requirementNo + input.caseNo),
|
||||||
|
assigneeId: input.assigneeId,
|
||||||
|
creatorId: input.assigneeId,
|
||||||
|
plannedTestAt: daysAfter(input.caseNo + 3),
|
||||||
|
plannedEndAt: daysAfter(input.caseNo + 4),
|
||||||
|
startedAt: status === 'pending' ? null : daysAfter(input.caseNo + 3),
|
||||||
|
completedAt: ['passed', 'failed', 'blocked'].includes(status) ? daysAfter(input.caseNo + 4) : null,
|
||||||
|
estimateHours: 2 + (input.caseNo % 4),
|
||||||
|
aiEstimateHours: 1 + (input.caseNo % 3),
|
||||||
|
references: [{ type: 'requirement', id: input.requirementId }],
|
||||||
|
aiDraft: false,
|
||||||
|
aiDraftAt: null,
|
||||||
|
createdAt: hoursAfter(input.requirementNo + input.caseNo),
|
||||||
|
updatedAt: hoursAfter(input.requirementNo + input.caseNo + 1),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildBug(input) {
|
||||||
|
const status = BUG_STATUSES[input.bugNo % BUG_STATUSES.length];
|
||||||
|
const severity = BUG_SEVERITIES[input.bugNo % BUG_SEVERITIES.length];
|
||||||
|
return {
|
||||||
|
id: `${ID_PREFIX}bug-${input.versionId}-${pad(input.bugNo, 5)}`,
|
||||||
|
versionId: input.versionId,
|
||||||
|
productId: input.productId,
|
||||||
|
projectId: input.projectId,
|
||||||
|
testCaseId: input.testCaseId ?? null,
|
||||||
|
testCaseVersionId: input.testCaseId ? input.versionId : null,
|
||||||
|
code: `BUG-${pad(input.bugNo, 5)}`,
|
||||||
|
title: `Bug ${pad(input.bugNo, 5)}`,
|
||||||
|
description: 'Performance fixture bug',
|
||||||
|
status,
|
||||||
|
severity,
|
||||||
|
priority: priority(input.bugNo),
|
||||||
|
assigneeId: input.assigneeId,
|
||||||
|
reporterId: input.assigneeId,
|
||||||
|
plannedFixAt: daysAfter(input.bugNo % 10),
|
||||||
|
resolvedAt: ['fixed', 'verifying', 'closed'].includes(status) ? daysAfter((input.bugNo % 10) + 1) : null,
|
||||||
|
closedAt: status === 'closed' ? daysAfter((input.bugNo % 10) + 2) : null,
|
||||||
|
resolution: status === 'closed' ? 'fixed' : null,
|
||||||
|
createdAt: hoursAfter(input.bugNo),
|
||||||
|
updatedAt: hoursAfter(input.bugNo + 1),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildWorkActivity(input) {
|
||||||
|
const source = input.source;
|
||||||
|
return {
|
||||||
|
id: `${ID_PREFIX}activity-${input.versionId}-${pad(input.activityNo, 5)}`,
|
||||||
|
versionId: input.versionId,
|
||||||
|
productId: input.productId,
|
||||||
|
projectId: input.projectId,
|
||||||
|
actorId: input.actor.id,
|
||||||
|
actorName: input.actor.name,
|
||||||
|
sourceType: 'dev_task',
|
||||||
|
sourceId: source?.id ?? `${ID_PREFIX}missing-source`,
|
||||||
|
sourceVersionId: input.versionId,
|
||||||
|
action: input.activityNo % 5 === 0 ? 'blocked' : 'progress',
|
||||||
|
title: `Activity ${pad(input.activityNo, 5)}`,
|
||||||
|
metadata: {
|
||||||
|
category: input.activityNo % 5 === 0 ? 'risk' : 'progress',
|
||||||
|
summary: `Performance activity ${input.activityNo}`,
|
||||||
|
},
|
||||||
|
occurredAt: hoursAfter(input.activityNo),
|
||||||
|
createdAt: hoursAfter(input.activityNo),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildXiaobaoSummary(input) {
|
||||||
|
const riskScore = Math.min(100, 40 + (input.bugCount % 50));
|
||||||
|
const riskLevel = riskScore >= 75 ? 'likely_delayed' : 'at_risk';
|
||||||
|
const riskSignature = `${input.version.id}|${riskLevel}|${riskScore}|${input.unfinishedCount}`;
|
||||||
|
return {
|
||||||
|
versionId: input.version.id,
|
||||||
|
riskLevel,
|
||||||
|
riskScore,
|
||||||
|
confidence: 72,
|
||||||
|
forecastReleaseDate: daysAfter(24),
|
||||||
|
riskSignature,
|
||||||
|
summary: {
|
||||||
|
versionId: input.version.id,
|
||||||
|
versionName: input.version.name,
|
||||||
|
riskLevel,
|
||||||
|
riskScore,
|
||||||
|
confidence: 72,
|
||||||
|
riskSignature,
|
||||||
|
dirty: false,
|
||||||
|
signals: {
|
||||||
|
unfinishedCount: input.unfinishedCount,
|
||||||
|
openBugCount: input.bugCount,
|
||||||
|
criticalBugCount: Math.floor(input.bugCount / 12),
|
||||||
|
failedTestCount: Math.floor(input.unfinishedCount / 8),
|
||||||
|
blockedCount: Math.floor(input.unfinishedCount / 15),
|
||||||
|
silentRiskCount: Math.floor(input.unfinishedCount / 20),
|
||||||
|
},
|
||||||
|
reasons: [
|
||||||
|
{
|
||||||
|
key: 'remaining_work',
|
||||||
|
title: 'Remaining fixture work',
|
||||||
|
detail: `Fixture has ${input.unfinishedCount} scoped requirements.`,
|
||||||
|
severity: 'warning',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
currentSnapshot: {
|
||||||
|
versionId: input.version.id,
|
||||||
|
date: BASE_DATE.toISOString().slice(0, 10),
|
||||||
|
riskScore,
|
||||||
|
riskLevel,
|
||||||
|
openBugCount: input.bugCount,
|
||||||
|
criticalBugCount: Math.floor(input.bugCount / 12),
|
||||||
|
failedTestCount: Math.floor(input.unfinishedCount / 8),
|
||||||
|
blockedCount: Math.floor(input.unfinishedCount / 15),
|
||||||
|
silentRiskCount: Math.floor(input.unfinishedCount / 20),
|
||||||
|
confidence: 72,
|
||||||
|
createdAt: BASE_DATE.toISOString(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dirty: false,
|
||||||
|
recomputedAt: BASE_DATE,
|
||||||
|
updatedAt: BASE_DATE,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function summarizeFixture(fixture) {
|
||||||
|
return {
|
||||||
|
fixtureVersion: FIXTURE_VERSION,
|
||||||
|
size: fixture.size,
|
||||||
|
anchors: fixture.anchors,
|
||||||
|
counts: Object.fromEntries(
|
||||||
|
Object.entries(fixture.rows).map(([key, rows]) => [key, rows.length]),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function seedDatabase(fixture, outputPath) {
|
||||||
|
const serverRequire = createRequire(new URL('../apps/server/package.json', import.meta.url));
|
||||||
|
const { PrismaClient } = serverRequire('@prisma/client');
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await clearPerfRows(prisma);
|
||||||
|
await prisma.user.createMany({ data: fixture.rows.users, skipDuplicates: true });
|
||||||
|
await prisma.product.createMany({ data: fixture.rows.products, skipDuplicates: true });
|
||||||
|
await prisma.project.createMany({ data: fixture.rows.projects, skipDuplicates: true });
|
||||||
|
await prisma.version.createMany({ data: fixture.rows.versions, skipDuplicates: true });
|
||||||
|
await prisma.requirement.createMany({ data: fixture.rows.requirements, skipDuplicates: true });
|
||||||
|
await prisma.versionPlan.createMany({ data: fixture.rows.versionPlans, skipDuplicates: true });
|
||||||
|
await prisma.devTask.createMany({ data: fixture.rows.devTasks, skipDuplicates: true });
|
||||||
|
await prisma.testCase.createMany({ data: fixture.rows.testCases, skipDuplicates: true });
|
||||||
|
await prisma.bug.createMany({ data: fixture.rows.bugs, skipDuplicates: true });
|
||||||
|
await prisma.workActivity.createMany({ data: fixture.rows.workActivities, skipDuplicates: true });
|
||||||
|
await prisma.xiaobaoRiskSummary.createMany({ data: fixture.rows.xiaobaoRiskSummaries, skipDuplicates: true });
|
||||||
|
} finally {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
const summary = summarizeFixture(fixture);
|
||||||
|
await mkdir(dirname(outputPath), { recursive: true });
|
||||||
|
await writeFile(outputPath, `${JSON.stringify(summary, null, 2)}\n`, 'utf8');
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearPerfRows(prisma) {
|
||||||
|
await prisma.$transaction([
|
||||||
|
prisma.xiaobaoRiskSummary.deleteMany({ where: { versionId: { startsWith: `${ID_PREFIX}version-` } } }),
|
||||||
|
prisma.workActivity.deleteMany({ where: { id: { startsWith: `${ID_PREFIX}activity-` } } }),
|
||||||
|
prisma.bug.deleteMany({ where: { id: { startsWith: `${ID_PREFIX}bug-` } } }),
|
||||||
|
prisma.testCase.deleteMany({ where: { id: { startsWith: `${ID_PREFIX}case-` } } }),
|
||||||
|
prisma.devTask.deleteMany({ where: { id: { startsWith: `${ID_PREFIX}dev-` } } }),
|
||||||
|
prisma.versionPlan.deleteMany({ where: { id: { startsWith: `${ID_PREFIX}plan-` } } }),
|
||||||
|
prisma.requirement.deleteMany({ where: { id: { startsWith: `${ID_PREFIX}req-` } } }),
|
||||||
|
prisma.version.deleteMany({ where: { id: { startsWith: `${ID_PREFIX}version-` } } }),
|
||||||
|
prisma.project.deleteMany({ where: { id: { startsWith: `${ID_PREFIX}project-` } } }),
|
||||||
|
prisma.product.deleteMany({ where: { id: { startsWith: `${ID_PREFIX}product-` } } }),
|
||||||
|
prisma.user.deleteMany({ where: { id: { startsWith: `${ID_PREFIX}user-` } } }),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const args = parseArgs(process.argv.slice(2));
|
||||||
|
const fixture = buildFixture(args.size);
|
||||||
|
const summary = summarizeFixture(fixture);
|
||||||
|
|
||||||
|
if (args.dryRun) {
|
||||||
|
if (args.json) {
|
||||||
|
console.log(JSON.stringify(summary, null, 2));
|
||||||
|
} else {
|
||||||
|
printSummary(summary, 'dry-run');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const outputPath = resolve(process.cwd(), args.output);
|
||||||
|
const seeded = await seedDatabase(fixture, outputPath);
|
||||||
|
if (args.json) {
|
||||||
|
console.log(JSON.stringify(seeded, null, 2));
|
||||||
|
} else {
|
||||||
|
printSummary(seeded, `seeded; metadata=${args.output}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function printSummary(summary, mode) {
|
||||||
|
console.log(`Large dataset fixture ${mode}`);
|
||||||
|
console.log(`version=${summary.fixtureVersion} size=${summary.size}`);
|
||||||
|
console.log(`anchors=${JSON.stringify(summary.anchors)}`);
|
||||||
|
for (const [name, count] of Object.entries(summary.counts)) {
|
||||||
|
console.log(`${name}=${count}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error(error instanceof Error ? error.message : error);
|
||||||
|
process.exitCode = 1;
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user