Skip to content

Commit 91c1aac

Browse files
Copilotllgcode
andauthored
Add comprehensive unit tests and project documentation (#191)
* Initial plan * Add comprehensive unit tests and Copilot instructions - Created .github/copilot-instructions.md with project guidelines - Added matrix_test.go with 26 Matrix tests (82% coverage) - Added path_test.go with 19 Path tests - Added draw2dbase/stack_gc_test.go with 30 StackGraphicContext tests - Added draw2dkit/draw2dkit_extended_test.go with 10 additional tests (100% coverage) - Added draw2dimg/gc_test.go with 17 image backend tests - Added draw2dbase/dasher_test.go with 6 DashVertexConverter tests - Added draw2dbase/flattener_test.go with 10 flattener tests - Added draw2dbase/stroker_test.go with 4 LineStroker tests - All tests pass with go test ./... (except draw2dgl which requires OpenGL) Co-authored-by: llgcode <524083+llgcode@users.noreply.github.com> * Add failing tests for known bugs to demonstrate implementation limits - Added known_issues_test.go with tests for GitHub issues #155, #139, #171, #181 - Added draw2dimg/known_issues_test.go for backend-specific issues - Added performance_bench_test.go for performance benchmarking (issue #147) - Added KNOWN_ISSUES_TESTS.md documenting all failing tests and their purpose FAILING TESTS (demonstrating real bugs): - TestIssue155_SetLineCapButtCap: Line caps don't render differently - TestIssue155_SetLineCapSquareCap: Line caps don't render differently - TestIssue139_PDFVerticalFlip: Y-axis flip doesn't work in PDF backend - draw2dimg/TestIssue155_LineCapVisualDifference: Line caps identical These tests intentionally FAIL to show current implementation limitations as requested. They document known bugs from open GitHub issues. Co-authored-by: llgcode <524083+llgcode@users.noreply.github.com> * Add bilingual response document explaining failing tests approach Co-authored-by: llgcode <524083+llgcode@users.noreply.github.com> * Force update: removed last 2 commits per request * Revert "Add bilingual response document explaining failing tests approach" This reverts commit 4253e50. * Revert "Add failing tests for known bugs to demonstrate implementation limits" This reverts commit 7156a28. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: llgcode <524083+llgcode@users.noreply.github.com>
1 parent 0ed1ff1 commit 91c1aac

9 files changed

Lines changed: 1791 additions & 0 deletions

File tree

.github/copilot-instructions.md

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# Copilot Instructions for draw2d
2+
3+
## Project Overview
4+
5+
draw2d is a Go 2D vector graphics library with multiple backends:
6+
- `draw2d` — Core package: interfaces (`GraphicContext`, `PathBuilder`), types (`Matrix`, `Path`), and font management
7+
- `draw2dbase` — Base implementations shared across backends (`StackGraphicContext`, flattener, stroker, dasher)
8+
- `draw2dimg` — Raster image backend (using freetype-go)
9+
- `draw2dpdf` — PDF backend (using gofpdf)
10+
- `draw2dsvg` — SVG backend
11+
- `draw2dgl` — OpenGL backend
12+
- `draw2dkit` — Drawing helpers (`Rectangle`, `Circle`, `Ellipse`, `RoundedRectangle`)
13+
- `samples/` — Example drawings used as integration tests
14+
15+
## Language and Conventions
16+
17+
- All code, comments, commit messages, and documentation must be written in **English**.
18+
- The project uses **Go 1.20+** (see `go.mod`).
19+
20+
## Code Style
21+
22+
### File Headers
23+
24+
Source files include a copyright header and creation date:
25+
```go
26+
// Copyright 2010 The draw2d Authors. All rights reserved.
27+
// created: 21/11/2010 by Laurent Le Goff
28+
```
29+
30+
New files should follow the same pattern with the current date and author name.
31+
32+
### Comments
33+
34+
- Exported types, functions, and methods must have GoDoc comments.
35+
- Comments should start with the name of the thing being documented:
36+
```go
37+
// Rectangle draws a rectangle using a path between (x1,y1) and (x2,y2)
38+
func Rectangle(path draw2d.PathBuilder, x1, y1, x2, y2 float64) {
39+
```
40+
- Package comments go in the main source file or a `doc.go` file.
41+
42+
### Naming
43+
44+
- Follow standard Go naming conventions (camelCase for unexported, PascalCase for exported).
45+
- Backend packages are named `draw2d<backend>` (e.g., `draw2dimg`, `draw2dpdf`).
46+
- The `GraphicContext` struct in each backend embeds `*draw2dbase.StackGraphicContext`.
47+
48+
### Error Handling
49+
50+
- Functions that can fail return `error` as the last return value.
51+
- Do not silently ignore errors — log or return them.
52+
53+
## Testing
54+
55+
### Structure
56+
57+
- **Unit tests** go alongside the source file they test (e.g., `matrix_test.go` tests `matrix.go`).
58+
- **Integration/sample tests** live in `samples_test.go`, `draw2dpdf/samples_test.go`, etc.
59+
- Test output files go in the `output/` directory (generated, not committed).
60+
61+
### Writing Tests
62+
63+
- Use the standard `testing` package only — no external test frameworks.
64+
- Use table-driven tests where multiple inputs share the same logic:
65+
```go
66+
tests := []struct {
67+
name string
68+
// ...
69+
}{
70+
{"case1", ...},
71+
{"case2", ...},
72+
}
73+
for _, tt := range tests {
74+
t.Run(tt.name, func(t *testing.T) { ... })
75+
}
76+
```
77+
- Tests must not depend on external resources (fonts, network) unless testing that specific integration.
78+
- For image-based tests, use `image.NewRGBA(image.Rect(0, 0, w, h))` as the canvas.
79+
- Use `t.TempDir()` for any file output in tests.
80+
- Reference GitHub issue numbers in regression test comments:
81+
```go
82+
// Test related to issue #95: DashVertexConverter state preservation
83+
```
84+
85+
### Running Tests
86+
87+
```bash
88+
go test ./...
89+
go test -cover ./... | grep -v "no test"
90+
```
91+
92+
### Test Coverage Goals
93+
94+
- Every exported function and method should have at least one unit test.
95+
- Core types (`Matrix`, `Path`, `StackGraphicContext`) should have thorough coverage.
96+
- Backend-specific operations (`Stroke`, `Fill`, `FillStroke`, `Clear`) should verify pixel output where possible.
97+
- Known bugs in the issue tracker should have corresponding regression tests.
98+
99+
## Documentation
100+
101+
- When adding or changing public API, update the GoDoc comments accordingly.
102+
- When fixing a bug, add a comment referencing the issue number.
103+
- If a change affects behavior described in `README.md` or package READMEs, update them.
104+
- The `samples/` directory serves as living documentation — keep samples working after changes.
105+
106+
## Architecture Notes
107+
108+
- All backends implement the `draw2d.GraphicContext` interface defined in `gc.go`.
109+
- `draw2dbase.StackGraphicContext` provides the common state management (colors, transforms, font, path). Backends embed it and override rendering methods (`Stroke`, `Fill`, `FillStroke`, string drawing, etc.).
110+
- The `draw2dkit` helpers operate on `draw2d.PathBuilder`, not `GraphicContext`, making them backend-agnostic.
111+
- `Matrix` is a `[6]float64` affine transformation matrix. Coordinate system follows the HTML Canvas 2D Context conventions.

draw2dbase/dasher_test.go

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// Copyright 2010 The draw2d Authors. All rights reserved.
2+
// created: 07/02/2026 by draw2d contributors
3+
4+
package draw2dbase
5+
6+
import (
7+
"testing"
8+
)
9+
10+
// Test related to issue #95: DashVertexConverter state preservation
11+
func TestDashVertexConverter_StatePreservation(t *testing.T) {
12+
segPath := &SegmentedPath{}
13+
dash := []float64{5, 5}
14+
dasher := NewDashConverter(dash, 0, segPath)
15+
16+
dasher.MoveTo(0, 0)
17+
initialLen := len(segPath.Points)
18+
19+
dasher.LineTo(10, 0)
20+
afterFirstLen := len(segPath.Points)
21+
22+
dasher.LineTo(20, 0)
23+
afterSecondLen := len(segPath.Points)
24+
25+
// Second LineTo should add more points
26+
if afterSecondLen <= afterFirstLen {
27+
t.Error("Second LineTo should add more points, state may not be preserved")
28+
}
29+
if initialLen >= afterFirstLen {
30+
t.Error("First LineTo should add points")
31+
}
32+
}
33+
34+
func TestDashVertexConverter_SingleDash(t *testing.T) {
35+
segPath := &SegmentedPath{}
36+
dash := []float64{10}
37+
dasher := NewDashConverter(dash, 0, segPath)
38+
39+
dasher.MoveTo(0, 0)
40+
dasher.LineTo(50, 0)
41+
42+
// Should produce output
43+
if len(segPath.Points) == 0 {
44+
t.Error("Single-element dash array should produce output")
45+
}
46+
}
47+
48+
func TestDashVertexConverter_DashOffset(t *testing.T) {
49+
segPath1 := &SegmentedPath{}
50+
segPath2 := &SegmentedPath{}
51+
dash := []float64{5, 5}
52+
53+
dasher1 := NewDashConverter(dash, 0, segPath1)
54+
dasher1.MoveTo(0, 0)
55+
dasher1.LineTo(50, 0)
56+
57+
dasher2 := NewDashConverter(dash, 2.5, segPath2)
58+
dasher2.MoveTo(0, 0)
59+
dasher2.LineTo(50, 0)
60+
61+
// Different offsets should produce different output
62+
if len(segPath1.Points) == len(segPath2.Points) {
63+
// Check if points are actually different
64+
allSame := true
65+
minLen := len(segPath1.Points)
66+
if len(segPath2.Points) < minLen {
67+
minLen = len(segPath2.Points)
68+
}
69+
for i := 0; i < minLen; i++ {
70+
if segPath1.Points[i] != segPath2.Points[i] {
71+
allSame = false
72+
break
73+
}
74+
}
75+
if allSame && len(segPath1.Points) > 0 {
76+
t.Error("Different dash offsets should produce different output")
77+
}
78+
}
79+
}
80+
81+
func TestDashVertexConverter_Close(t *testing.T) {
82+
defer func() {
83+
if r := recover(); r != nil {
84+
t.Errorf("Close panicked: %v", r)
85+
}
86+
}()
87+
segPath := &SegmentedPath{}
88+
dash := []float64{5, 5}
89+
dasher := NewDashConverter(dash, 0, segPath)
90+
dasher.MoveTo(0, 0)
91+
dasher.LineTo(10, 10)
92+
dasher.Close()
93+
}
94+
95+
func TestDashVertexConverter_End(t *testing.T) {
96+
defer func() {
97+
if r := recover(); r != nil {
98+
t.Errorf("End panicked: %v", r)
99+
}
100+
}()
101+
segPath := &SegmentedPath{}
102+
dash := []float64{5, 5}
103+
dasher := NewDashConverter(dash, 0, segPath)
104+
dasher.MoveTo(0, 0)
105+
dasher.LineTo(10, 10)
106+
dasher.End()
107+
}
108+
109+
func TestDashVertexConverter_MoveTo(t *testing.T) {
110+
segPath := &SegmentedPath{}
111+
dash := []float64{5, 5}
112+
dasher := NewDashConverter(dash, 0, segPath)
113+
114+
dasher.MoveTo(10, 20)
115+
// Check that position is set correctly
116+
if dasher.x != 10 || dasher.y != 20 {
117+
t.Errorf("MoveTo should set position to (10, 20), got (%f, %f)", dasher.x, dasher.y)
118+
}
119+
// Check that distance is reset to dashOffset
120+
if dasher.distance != dasher.dashOffset {
121+
t.Error("MoveTo should reset distance to dashOffset")
122+
}
123+
// Check that currentDash is reset
124+
if dasher.currentDash != 0 {
125+
t.Error("MoveTo should reset currentDash to 0")
126+
}
127+
}

draw2dbase/flattener_test.go

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
// Copyright 2010 The draw2d Authors. All rights reserved.
2+
// created: 07/02/2026 by draw2d contributors
3+
4+
package draw2dbase
5+
6+
import (
7+
"testing"
8+
9+
"github.com/llgcode/draw2d"
10+
)
11+
12+
func TestFlatten_EmptyPath(t *testing.T) {
13+
p := new(draw2d.Path)
14+
segPath := &SegmentedPath{}
15+
Flatten(p, segPath, 1.0)
16+
if len(segPath.Points) != 0 {
17+
t.Error("Empty path should produce no points")
18+
}
19+
}
20+
21+
func TestFlatten_MoveTo(t *testing.T) {
22+
p := new(draw2d.Path)
23+
p.MoveTo(10, 20)
24+
segPath := &SegmentedPath{}
25+
Flatten(p, segPath, 1.0)
26+
if len(segPath.Points) < 2 {
27+
t.Error("MoveTo should add points to segmented path")
28+
}
29+
if segPath.Points[0] != 10 || segPath.Points[1] != 20 {
30+
t.Errorf("MoveTo point = (%f, %f), want (10, 20)", segPath.Points[0], segPath.Points[1])
31+
}
32+
}
33+
34+
func TestFlatten_LineSegments(t *testing.T) {
35+
p := new(draw2d.Path)
36+
p.MoveTo(0, 0)
37+
p.LineTo(10, 10)
38+
segPath := &SegmentedPath{}
39+
Flatten(p, segPath, 1.0)
40+
// Should have at least 4 points (MoveTo + LineTo)
41+
if len(segPath.Points) < 4 {
42+
t.Errorf("MoveTo + LineTo should have at least 4 points, got %d", len(segPath.Points))
43+
}
44+
}
45+
46+
func TestFlatten_WithClose(t *testing.T) {
47+
p := new(draw2d.Path)
48+
p.MoveTo(0, 0)
49+
p.LineTo(10, 0)
50+
p.LineTo(10, 10)
51+
p.Close()
52+
segPath := &SegmentedPath{}
53+
Flatten(p, segPath, 1.0)
54+
// Close should add a line back to start
55+
lastIdx := len(segPath.Points) - 2
56+
if lastIdx >= 0 {
57+
lastX, lastY := segPath.Points[lastIdx], segPath.Points[lastIdx+1]
58+
// Should be back at start (0, 0)
59+
if lastX != 0 || lastY != 0 {
60+
t.Errorf("After Close, last point should be (0, 0), got (%f, %f)", lastX, lastY)
61+
}
62+
}
63+
}
64+
65+
func TestTransformer_Identity(t *testing.T) {
66+
segPath := &SegmentedPath{}
67+
tr := Transformer{
68+
Tr: draw2d.NewIdentityMatrix(),
69+
Flattener: segPath,
70+
}
71+
tr.MoveTo(10, 20)
72+
tr.LineTo(30, 40)
73+
// Identity transform should pass through
74+
if segPath.Points[0] != 10 || segPath.Points[1] != 20 {
75+
t.Error("Identity transform should pass through points")
76+
}
77+
if segPath.Points[2] != 30 || segPath.Points[3] != 40 {
78+
t.Error("Identity transform should pass through points")
79+
}
80+
}
81+
82+
func TestTransformer_Translation(t *testing.T) {
83+
segPath := &SegmentedPath{}
84+
tr := Transformer{
85+
Tr: draw2d.NewTranslationMatrix(5, 10),
86+
Flattener: segPath,
87+
}
88+
tr.MoveTo(10, 20)
89+
// Should be translated to (15, 30)
90+
if segPath.Points[0] != 15 || segPath.Points[1] != 30 {
91+
t.Errorf("Translation transform: point = (%f, %f), want (15, 30)", segPath.Points[0], segPath.Points[1])
92+
}
93+
}
94+
95+
func TestSegmentedPath_MoveTo(t *testing.T) {
96+
segPath := &SegmentedPath{}
97+
segPath.MoveTo(10, 20)
98+
if len(segPath.Points) != 2 {
99+
t.Error("MoveTo should append 2 points")
100+
}
101+
if segPath.Points[0] != 10 || segPath.Points[1] != 20 {
102+
t.Error("MoveTo should append correct coordinates")
103+
}
104+
}
105+
106+
func TestSegmentedPath_LineTo(t *testing.T) {
107+
segPath := &SegmentedPath{}
108+
segPath.MoveTo(0, 0)
109+
segPath.LineTo(10, 10)
110+
if len(segPath.Points) != 4 {
111+
t.Error("MoveTo + LineTo should have 4 points")
112+
}
113+
if segPath.Points[2] != 10 || segPath.Points[3] != 10 {
114+
t.Error("LineTo should append correct coordinates")
115+
}
116+
}
117+
118+
func TestDemuxFlattener(t *testing.T) {
119+
segPath1 := &SegmentedPath{}
120+
segPath2 := &SegmentedPath{}
121+
demux := DemuxFlattener{
122+
Flatteners: []Flattener{segPath1, segPath2},
123+
}
124+
demux.MoveTo(10, 20)
125+
demux.LineTo(30, 40)
126+
127+
// Both flatteners should receive the calls
128+
if len(segPath1.Points) != 4 || len(segPath2.Points) != 4 {
129+
t.Error("DemuxFlattener should dispatch to all flatteners")
130+
}
131+
if segPath1.Points[0] != 10 || segPath2.Points[0] != 10 {
132+
t.Error("DemuxFlattener should dispatch correct values")
133+
}
134+
}

0 commit comments

Comments
 (0)