feat: 初始版本 - ttychart-mcp 终端图表 MCP 服务

- 支持三种图表: 折线图、柱状图、散点图
- MCP 协议支持 (stdio + HTTP)
- 完整的单元测试和集成测试
- Docker 支持
- Makefile 构建脚本
This commit is contained in:
2026-04-15 21:03:36 +08:00
commit d3e04c5d1a
15 changed files with 2038 additions and 0 deletions

284
tools/charts/line_test.go Normal file
View File

@@ -0,0 +1,284 @@
// Package charts 图表单元测试
//
// 对所有图表类型进行单元测试
//
// 作者: titor
// 创建日期: 2026-04-15
package charts
import (
"testing"
)
// TestLineChart_NewLineChart 测试创建折线图
func TestLineChart_NewLineChart(t *testing.T) {
// 测试正常创建
chart := NewLineChart(50, 10)
if chart == nil {
t.Error("NewLineChart 返回 nil")
}
// 测试零值创建
chart0 := NewLineChart(0, 0)
if chart0 == nil {
t.Error("NewLineChart(0,0) 返回 nil")
}
}
// TestLineChart_SetData 测试设置数据
func TestLineChart_SetData(t *testing.T) {
chart := NewLineChart(0, 0)
// 测试设置数据
data := []float64{1.0, 2.0, 3.0, 4.0, 5.0}
chart.SetData(data)
// 验证数据设置正确
if len(chart.Data) != len(data) {
t.Errorf("数据长度不匹配: got %d, want %d", len(chart.Data), len(data))
}
// 测试空数据
chart.SetData(nil)
if chart.Data != nil {
t.Error("设置 nil 后数据不为 nil")
}
}
// TestLineChart_SetTitle 测试设置标题
func TestLineChart_SetTitle(t *testing.T) {
chart := NewLineChart(0, 0)
// 测试设置标题
title := "测试折线图"
chart.SetTitle(title)
// 验证标题设置正确
if chart.Title != title {
t.Errorf("标题不匹配: got %s, want %s", chart.Title, title)
}
}
// TestLineChart_SetLabels 测试设置标签
func TestLineChart_SetLabels(t *testing.T) {
chart := NewLineChart(0, 0)
// 测试设置标签
labels := []string{"周一", "周二", "周三", "周四", "周五"}
chart.SetLabels(labels)
// 验证标签设置正确
if len(chart.Labels) != len(labels) {
t.Errorf("标签长度不匹配: got %d, want %d", len(chart.Labels), len(labels))
}
}
// TestLineChart_Render 测试渲染折线图
func TestLineChart_Render(t *testing.T) {
// 测试空数据渲染
chart := NewLineChart(30, 10)
result := chart.Render()
if result == "" {
t.Error("空数据渲染结果为空")
}
// 测试有数据渲染
chart.SetData([]float64{10, 20, 15, 25, 30})
result = chart.Render()
if result == "" {
t.Error("有数据渲染结果为空")
}
// 测试有标题渲染
chart.SetTitle("温度趋势")
result = chart.Render()
if result == "" {
t.Error("有标题渲染结果为空")
}
// 验证包含标题
if len(result) < len("温度趋势") {
t.Error("渲染结果长度异常")
}
}
// TestLineChart_findMinMax 测试查找最小最大值
func TestLineChart_findMinMax(t *testing.T) {
chart := NewLineChart(0, 0)
// 测试空数据
minVal, maxVal := chart.findMinMax()
if minVal != 0 || maxVal != 1 {
t.Errorf("空数据范围错误: min=%f, max=%f", minVal, maxVal)
}
// 测试单点数据
chart.SetData([]float64{5.0})
minVal, maxVal = chart.findMinMax()
if minVal == maxVal {
t.Error("单点数据 min 和 max 相同")
}
// 测试多点数据
chart.SetData([]float64{1.0, 5.0, 3.0, 8.0, 2.0})
minVal, maxVal = chart.findMinMax()
if minVal >= maxVal {
t.Errorf("数据范围错误: min=%f, max=%f", minVal, maxVal)
}
}
// TestBarChart_NewBarChart 测试创建柱状图
func TestBarChart_NewBarChart(t *testing.T) {
// 测试垂直柱状图
barV := NewBarChart(60, 10, false)
if barV == nil {
t.Error("NewBarChart 垂直返回 nil")
}
// 测试水平柱状图
barH := NewBarChart(60, 10, true)
if barH == nil {
t.Error("NewBarChart 水平返回 nil")
}
}
// TestBarChart_SetData 测试设置柱状图数据
func TestBarChart_SetData(t *testing.T) {
chart := NewBarChart(60, 10, false)
// 测试设置数据
data := []BarDataItem{
{Label: "苹果", Value: 100},
{Label: "香蕉", Value: 80},
{Label: "橙子", Value: 120},
}
chart.SetData(data)
// 验证数据设置正确
if len(chart.Data) != len(data) {
t.Errorf("数据长度不匹配: got %d, want %d", len(chart.Data), len(data))
}
// 测试空数据
chart.SetData(nil)
if chart.Data != nil {
t.Error("设置 nil 后数据不为 nil")
}
}
// TestBarChart_Render 测试渲染柱状图
func TestBarChart_Render(t *testing.T) {
// 测试空数据渲染
chart := NewBarChart(60, 10, false)
result := chart.Render()
if result == "" {
t.Error("空数据渲染结果为空")
}
// 测试有数据渲染
chart.SetData([]BarDataItem{
{Label: "苹果", Value: 100},
{Label: "香蕉", Value: 80},
})
result = chart.Render()
if result == "" {
t.Error("有数据渲染结果为空")
}
// 测试有标题渲染
chart.SetTitle("水果销量")
result = chart.Render()
if result == "" {
t.Error("有标题渲染结果为空")
}
// 测试水平柱状图
chartH := NewBarChart(60, 10, true)
chartH.SetData([]BarDataItem{
{Label: "A", Value: 50},
{Label: "B", Value: 30},
})
result = chartH.Render()
if result == "" {
t.Error("水平柱状图渲染结果为空")
}
}
// TestScatterChart_NewScatterChart 测试创建散点图
func TestScatterChart_NewScatterChart(t *testing.T) {
chart := NewScatterChart(50, 20)
if chart == nil {
t.Error("NewScatterChart 返回 nil")
}
}
// TestScatterChart_SetData 测试设置散点图数据
func TestScatterChart_SetData(t *testing.T) {
chart := NewScatterChart(50, 20)
// 测试设置数据
data := []ScatterDataItem{
{X: 1.0, Y: 5.0},
{X: 2.0, Y: 8.0},
{X: 3.0, Y: 3.0},
}
chart.SetData(data)
// 验证数据设置正确
if len(chart.Data) != len(data) {
t.Errorf("数据长度不匹配: got %d, want %d", len(chart.Data), len(data))
}
}
// TestScatterChart_Render 测试渲染散点图
func TestScatterChart_Render(t *testing.T) {
// 测试空数据渲染
chart := NewScatterChart(50, 20)
result := chart.Render()
if result == "" {
t.Error("空数据渲染结果为空")
}
// 测试有数据渲染
chart.SetData([]ScatterDataItem{
{X: 1.0, Y: 5.0},
{X: 2.0, Y: 8.0},
{X: 3.0, Y: 3.0},
})
result = chart.Render()
if result == "" {
t.Error("有数据渲染结果为空")
}
// 测试有标题渲染
chart.SetTitle("成绩分布")
result = chart.Render()
if result == "" {
t.Error("有标题渲染结果为空")
}
}
// TestScatterChart_findMinMax 测试散点图最小最大值
func TestScatterChart_findMinMax(t *testing.T) {
chart := NewScatterChart(50, 20)
// 测试空数据
minX, maxX := chart.findMinMaxX()
minY, maxY := chart.findMinMaxY()
if minX != 0 || maxX != 1 || minY != 0 || maxY != 1 {
t.Errorf("空数据范围错误")
}
// 测试有数据
chart.SetData([]ScatterDataItem{
{X: 1.0, Y: 5.0},
{X: 2.0, Y: 8.0},
{X: 3.0, Y: 3.0},
})
minX, maxX = chart.findMinMaxX()
minY, maxY = chart.findMinMaxY()
if minX >= maxX || minY >= maxY {
t.Error("数<><E695B0><EFBFBD>范围错误")
}
}