XNSim/XNSimHtml/components/run-test.js
2025-04-28 12:25:20 +08:00

808 lines
28 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

class RunTest extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.scenarioFiles = [];
this.currentScenario = null;
this.modelGroups = [];
this.services = [];
this.currentSimulationId = null;
this.eventSource = null;
}
connectedCallback() {
this.fetchScenarioFiles();
this.render();
}
async fetchScenarioFiles() {
try {
const response = await fetch('/api/scenario-files');
if (!response.ok) {
throw new Error('无法获取场景文件');
}
this.scenarioFiles = await response.json();
this.updateScenarioDropdown();
} catch (error) {
console.error('获取场景文件失败:', error);
this.showError('获取场景文件失败: ' + error.message);
}
}
updateScenarioDropdown() {
const dropdown = this.shadowRoot.querySelector('#scenario-select');
if (!dropdown) return;
// 清空现有选项
dropdown.innerHTML = '';
// 添加提示选项
const defaultOption = document.createElement('option');
defaultOption.value = '';
defaultOption.textContent = '-- 选择配置文件 --';
defaultOption.disabled = true;
defaultOption.selected = true;
dropdown.appendChild(defaultOption);
// 添加场景文件选项
this.scenarioFiles.forEach(file => {
const option = document.createElement('option');
option.value = file.path;
option.textContent = file.name;
dropdown.appendChild(option);
});
}
async onScenarioSelected(event) {
const selectedScenarioPath = event.target.value;
if (!selectedScenarioPath) {
return;
}
try {
this.showMessage('加载配置文件内容...');
// 使用file-content API获取XML内容
const response = await fetch(`/api/file-content?path=${encodeURIComponent(selectedScenarioPath)}`);
if (!response.ok) {
throw new Error('无法获取配置文件内容');
}
const xmlContent = await response.text();
this.parseScenarioXML(xmlContent);
this.currentScenario = selectedScenarioPath;
this.updateModelAndServiceLists();
this.showMessage('配置文件加载完成');
} catch (error) {
console.error('加载配置文件失败:', error);
this.showError('加载配置文件失败: ' + error.message);
}
}
parseScenarioXML(xmlContent) {
try {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xmlContent, "text/xml");
// 解析模型组和模型
this.modelGroups = [];
const modelGroupElements = xmlDoc.querySelectorAll('ModelGroup');
modelGroupElements.forEach(groupElem => {
const groupName = groupElem.getAttribute('Name');
const models = [];
groupElem.querySelectorAll('Model').forEach(modelElem => {
models.push({
name: modelElem.getAttribute('Name'),
className: modelElem.getAttribute('ClassName')
});
});
this.modelGroups.push({
name: groupName,
models: models
});
});
// 解析服务
this.services = [];
const serviceElements = xmlDoc.querySelectorAll('ServicesList > Service');
serviceElements.forEach(serviceElem => {
this.services.push({
name: serviceElem.getAttribute('Name'),
className: serviceElem.getAttribute('ClassName')
});
});
} catch (error) {
console.error('解析XML失败:', error);
throw new Error('解析XML失败: ' + error.message);
}
}
updateModelAndServiceLists() {
// 更新模型列表
const modelListContainer = this.shadowRoot.querySelector('#model-list');
modelListContainer.innerHTML = '';
this.modelGroups.forEach(group => {
const groupItem = document.createElement('div');
groupItem.className = 'list-group';
const groupHeader = document.createElement('div');
groupHeader.className = 'list-group-header';
groupHeader.textContent = group.name;
groupItem.appendChild(groupHeader);
const modelsList = document.createElement('div');
modelsList.className = 'list-items';
group.models.forEach(model => {
const modelItem = document.createElement('div');
modelItem.className = 'list-item';
modelItem.textContent = model.name;
modelsList.appendChild(modelItem);
});
groupItem.appendChild(modelsList);
modelListContainer.appendChild(groupItem);
});
// 更新服务列表
const serviceListContainer = this.shadowRoot.querySelector('#service-list');
serviceListContainer.innerHTML = '';
this.services.forEach(service => {
const serviceItem = document.createElement('div');
serviceItem.className = 'list-item';
serviceItem.textContent = service.name;
serviceListContainer.appendChild(serviceItem);
});
}
async runTest() {
const selectedScenario = this.shadowRoot.querySelector('#scenario-select').value;
if (!selectedScenario) {
this.showError('请先选择配置文件');
return;
}
try {
this.showMessage('准备运行测试...');
const runButton = this.shadowRoot.querySelector('#run-button');
runButton.disabled = true;
runButton.textContent = '运行中...';
// 显示终止按钮
const stopButton = this.shadowRoot.querySelector('#stop-button');
if (stopButton) {
stopButton.style.display = 'block';
}
// 清空并初始化输出框
const outputContent = this.shadowRoot.querySelector('#output-content');
outputContent.innerHTML = '开始执行测试...\n';
// 关闭之前的EventSource连接
if (this.eventSource) {
this.eventSource.close();
this.eventSource = null;
}
// 保存当前仿真信息
this.currentSimulationId = Date.now().toString();
// 准备启动参数
const simulationArgs = [selectedScenario, '-test'];
// 调用后端API执行仿真
const response = await fetch('/api/run-simulation', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
id: this.currentSimulationId,
args: simulationArgs,
timeout: 120000 // 2分钟超时
})
});
if (!response.ok) {
const errorData = await response.json();
outputContent.innerHTML += `\n错误: ${errorData.message || '测试执行失败'}\n`;
if (errorData.output) outputContent.innerHTML += this.convertAnsiToHtml(errorData.output);
if (errorData.errorOutput) outputContent.innerHTML += this.convertAnsiToHtml(errorData.errorOutput);
throw new Error(errorData.message || '测试执行失败');
}
const responseData = await response.json();
// 获取仿真ID
const simulationId = responseData.simulationId || this.currentSimulationId;
this.currentSimulationId = simulationId;
// 设置SSE连接获取实时输出
this.connectToEventSource(simulationId);
this.showSuccess(`测试已启动`);
} catch (error) {
console.error('执行测试失败:', error);
this.showError('执行测试失败: ' + error.message);
// 显示错误详情
const outputContent = this.shadowRoot.querySelector('#output-content');
outputContent.innerHTML += `\n\n执行错误: ${error.message}`;
// 重置UI
this.resetUIAfterCompletion();
}
}
// 连接到SSE事件源获取实时输出
connectToEventSource(simulationId) {
// 关闭之前的连接
this.closeEventSource();
// 创建新的SSE连接
const url = `/api/simulation-output/${simulationId}`;
this.eventSource = new EventSource(url);
// 标准输出和错误输出
this.eventSource.addEventListener('output', (event) => {
try {
const data = JSON.parse(event.data);
const outputContent = this.shadowRoot.querySelector('#output-content');
// 添加新输出并应用ANSI颜色
if (data.data) {
const html = this.convertAnsiToHtml(data.data);
outputContent.innerHTML += html;
// 自动滚动到底部
outputContent.scrollTop = outputContent.scrollHeight;
}
} catch (error) {
console.error('处理输出事件失败:', error);
}
});
// 仿真状态
this.eventSource.addEventListener('status', (event) => {
try {
const data = JSON.parse(event.data);
if (data.running === false) {
// 仿真已经不存在或已结束
this.showMessage(data.message || '测试不存在或已结束');
// 重置UI
this.resetUIAfterCompletion();
}
} catch (error) {
console.error('处理状态事件失败:', error);
}
});
// 仿真完成
this.eventSource.addEventListener('completed', (event) => {
try {
const data = JSON.parse(event.data);
if (data.success) {
this.showSuccess('测试执行成功');
} else {
this.showError(`测试执行失败: ${data.message}`);
}
// 重置UI
this.resetUIAfterCompletion();
} catch (error) {
console.error('处理完成事件失败:', error);
}
});
// 仿真错误
this.eventSource.addEventListener('error', (event) => {
try {
const data = JSON.parse(event.data);
this.showError(`测试错误: ${data.message}`);
// 重置UI
this.resetUIAfterCompletion();
} catch (error) {
console.error('处理错误事件失败:', error);
}
});
// 仿真终止
this.eventSource.addEventListener('terminated', (event) => {
try {
const data = JSON.parse(event.data);
this.showMessage(`测试已终止: ${data.message}`);
// 在输出框中添加终止信息
const outputContent = this.shadowRoot.querySelector('#output-content');
outputContent.innerHTML += `\n\n测试已终止:${data.message}`;
// 重置UI
this.resetUIAfterCompletion();
} catch (error) {
console.error('处理终止事件失败:', error);
}
});
// 仿真超时
this.eventSource.addEventListener('timeout', (event) => {
try {
const data = JSON.parse(event.data);
this.showError(`测试超时: ${data.message}`);
// 在输出框中添加超时信息
const outputContent = this.shadowRoot.querySelector('#output-content');
outputContent.innerHTML += `\n\n测试超时:${data.message}`;
// 重置UI
this.resetUIAfterCompletion();
} catch (error) {
console.error('处理超时事件失败:', error);
}
});
// 连接错误
this.eventSource.onerror = (error) => {
console.error('SSE连接错误:', error);
// 检查是否已经清理了资源
if (this.eventSource && this.eventSource.readyState === 2) { // CLOSED
this.showError('实时输出连接已断开');
// 重置UI
this.resetUIAfterCompletion();
}
};
}
// 关闭SSE连接
closeEventSource() {
if (this.eventSource) {
this.eventSource.close();
this.eventSource = null;
}
}
// 在仿真完成后重置UI
resetUIAfterCompletion() {
// 隐藏终止按钮
const stopButton = this.shadowRoot.querySelector('#stop-button');
if (stopButton) {
stopButton.style.display = 'none';
}
// 重置运行按钮
const runButton = this.shadowRoot.querySelector('#run-button');
if (runButton) {
runButton.disabled = false;
runButton.textContent = '运行测试';
}
// 关闭SSE连接
this.closeEventSource();
// 清除当前仿真ID
this.currentSimulationId = null;
}
async stopSimulation() {
if (!this.currentSimulationId) {
this.showMessage('没有正在运行的测试');
return;
}
try {
this.showMessage('正在终止测试...');
const response = await fetch('/api/stop-simulation', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
id: this.currentSimulationId
})
});
if (response.ok) {
const result = await response.json();
this.showSuccess(`${result.message || '测试已终止'}`);
// 终止信息会通过SSE推送到UI
// 此处不需要额外操作
} else {
const errorData = await response.json();
this.showError(`终止测试失败: ${errorData.message || '未知错误'}`);
// 可能是SSE已断开手动重置UI
this.resetUIAfterCompletion();
}
} catch (error) {
console.error('终止测试失败:', error);
this.showError('终止测试失败: ' + error.message);
// 手动重置UI
this.resetUIAfterCompletion();
}
}
showError(message) {
const messageElement = this.shadowRoot.querySelector('#message');
messageElement.textContent = message;
messageElement.style.color = 'red';
}
showMessage(message) {
const messageElement = this.shadowRoot.querySelector('#message');
messageElement.textContent = message;
messageElement.style.color = 'blue';
}
showSuccess(message) {
const messageElement = this.shadowRoot.querySelector('#message');
messageElement.textContent = message;
messageElement.style.color = 'green';
}
render() {
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
height: 100%;
overflow: auto;
padding: 16px;
box-sizing: border-box;
}
.test-container {
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
padding: 16px;
height: 100%;
box-sizing: border-box;
display: flex;
flex-direction: column;
}
.config-selector {
display: flex;
align-items: center;
margin-bottom: 20px;
gap: 10px;
}
.selector-label {
min-width: 120px;
font-weight: bold;
color: #333;
}
select {
flex: 1;
padding: 8px 12px;
border-radius: 4px;
border: 1px solid #ccc;
}
button {
padding: 8px 16px;
background-color: #1976d2;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
min-width: 100px;
}
button:hover {
background-color: #1565c0;
}
button:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
#stop-button {
background-color: #d32f2f;
display: none;
margin-left: 10px;
}
#stop-button:hover {
background-color: #b71c1c;
}
#message {
margin: 10px 0;
padding: 10px;
border-radius: 4px;
}
.content-container {
display: flex;
flex: 1;
gap: 20px;
overflow: hidden;
}
.lists-container {
width: 250px;
display: flex;
flex-direction: column;
gap: 15px;
overflow: auto;
}
.list-section {
border: 1px solid #e0e0e0;
border-radius: 4px;
display: flex;
flex-direction: column;
overflow: hidden;
}
.list-header {
background-color: #f5f5f5;
padding: 10px;
font-weight: bold;
border-bottom: 1px solid #e0e0e0;
}
.list-content {
overflow: auto;
padding: 10px;
max-height: 200px;
}
.list-group {
margin-bottom: 15px;
}
.list-group-header {
font-weight: bold;
color: #333;
margin-bottom: 5px;
padding-bottom: 3px;
border-bottom: 1px solid #eee;
}
.list-items {
padding-left: 15px;
}
.list-item {
padding: 5px 0;
}
.output-container {
flex: 1;
display: flex;
flex-direction: column;
border: 1px solid #e0e0e0;
border-radius: 4px;
overflow: hidden;
}
.output-header {
background-color: #f5f5f5;
padding: 10px;
font-weight: bold;
border-bottom: 1px solid #e0e0e0;
}
.output-content {
flex: 1;
background-color: #2b2b2b;
color: #e0e0e0;
font-family: monospace;
padding: 10px;
overflow: auto;
white-space: pre-wrap;
line-height: 1.4;
}
/* 终端颜色支持 */
.output-content .ansi-black { color: #000000; }
.output-content .ansi-red { color: #ff0000; }
.output-content .ansi-green { color: #00ff00; }
.output-content .ansi-yellow { color: #ffff00; }
.output-content .ansi-blue { color: #0000ff; }
.output-content .ansi-magenta { color: #ff00ff; }
.output-content .ansi-cyan { color: #00ffff; }
.output-content .ansi-white { color: #ffffff; }
.output-content .ansi-bright-black { color: #808080; }
.output-content .ansi-bright-red { color: #ff5555; }
.output-content .ansi-bright-green { color: #55ff55; }
.output-content .ansi-bright-yellow { color: #ffff55; }
.output-content .ansi-bright-blue { color: #5555ff; }
.output-content .ansi-bright-magenta { color: #ff55ff; }
.output-content .ansi-bright-cyan { color: #55ffff; }
.output-content .ansi-bright-white { color: #ffffff; }
.output-content .ansi-bg-black { background-color: #000000; }
.output-content .ansi-bg-red { background-color: #ff0000; }
.output-content .ansi-bg-green { background-color: #00ff00; }
.output-content .ansi-bg-yellow { background-color: #ffff00; }
.output-content .ansi-bg-blue { background-color: #0000ff; }
.output-content .ansi-bg-magenta { background-color: #ff00ff; }
.output-content .ansi-bg-cyan { background-color: #00ffff; }
.output-content .ansi-bg-white { background-color: #ffffff; }
.output-content .ansi-bold { font-weight: bold; }
.output-content .ansi-italic { font-style: italic; }
.output-content .ansi-underline { text-decoration: underline; }
.buttons-container {
display: flex;
}
</style>
<div class="test-container">
<div class="config-selector">
<div class="selector-label">选择运行环境配置:</div>
<select id="scenario-select">
<option value="" disabled selected>-- 加载配置文件中... --</option>
</select>
<div class="buttons-container">
<button id="run-button">运行测试</button>
<button id="stop-button">终止测试</button>
</div>
</div>
<div id="message"></div>
<div class="content-container">
<div class="lists-container">
<div class="list-section">
<div class="list-header">模型列表</div>
<div id="model-list" class="list-content"></div>
</div>
<div class="list-section">
<div class="list-header">服务列表</div>
<div id="service-list" class="list-content"></div>
</div>
</div>
<div class="output-container">
<div class="output-header">运行输出</div>
<div id="output-content" class="output-content">等待测试运行...</div>
</div>
</div>
</div>
`;
// 添加事件监听器
const runButton = this.shadowRoot.querySelector('#run-button');
runButton.addEventListener('click', () => this.runTest());
const stopButton = this.shadowRoot.querySelector('#stop-button');
stopButton.addEventListener('click', () => this.stopSimulation());
const scenarioSelect = this.shadowRoot.querySelector('#scenario-select');
scenarioSelect.addEventListener('change', (event) => this.onScenarioSelected(event));
}
// ANSI终端颜色转换为HTML
convertAnsiToHtml(text) {
if (!text) return '';
// 映射ANSI颜色代码到CSS类名
const ansiToHtmlClass = {
// 前景色 (30-37)
30: 'ansi-black',
31: 'ansi-red',
32: 'ansi-green',
33: 'ansi-yellow',
34: 'ansi-blue',
35: 'ansi-magenta',
36: 'ansi-cyan',
37: 'ansi-white',
// 明亮前景色 (90-97)
90: 'ansi-bright-black',
91: 'ansi-bright-red',
92: 'ansi-bright-green',
93: 'ansi-bright-yellow',
94: 'ansi-bright-blue',
95: 'ansi-bright-magenta',
96: 'ansi-bright-cyan',
97: 'ansi-bright-white',
// 背景色 (40-47)
40: 'ansi-bg-black',
41: 'ansi-bg-red',
42: 'ansi-bg-green',
43: 'ansi-bg-yellow',
44: 'ansi-bg-blue',
45: 'ansi-bg-magenta',
46: 'ansi-bg-cyan',
47: 'ansi-bg-white',
// 文字样式
1: 'ansi-bold',
3: 'ansi-italic',
4: 'ansi-underline'
};
// 替换ANSI转义序列
const parts = [];
let currentSpan = null;
let currentClasses = [];
// 先分割ANSI序列和文本
const regex = /\x1b\[([\d;]*)m/g;
let lastIndex = 0;
let match;
while ((match = regex.exec(text)) !== null) {
// 添加匹配前的文本
if (match.index > lastIndex) {
parts.push({
text: text.substring(lastIndex, match.index),
classes: [...currentClasses]
});
}
// 处理ANSI代码
const codes = match[1].split(';').map(Number);
// 重置所有样式
if (codes.includes(0) || codes.length === 0) {
currentClasses = [];
} else {
// 添加样式
for (const code of codes) {
if (ansiToHtmlClass[code]) {
currentClasses.push(ansiToHtmlClass[code]);
}
}
}
lastIndex = regex.lastIndex;
}
// 添加最后一段文本
if (lastIndex < text.length) {
parts.push({
text: text.substring(lastIndex),
classes: [...currentClasses]
});
}
// 生成HTML
let html = '';
for (const part of parts) {
if (part.text) {
if (part.classes.length > 0) {
html += `<span class="${part.classes.join(' ')}">${this.escapeHtml(part.text)}</span>`;
} else {
html += this.escapeHtml(part.text);
}
}
}
return html;
}
// 转义HTML特殊字符
escapeHtml(text) {
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
}
customElements.define('run-test', RunTest);