Skip to content

Commit 4630ad6

Browse files
committed
Improve test coverage across cmd, exec, and markdown packages
Add unit tests for previously uncovered or undercovered code paths: - cmd.Pop: new pop_test.go with 6 test cases (was 0% package-level coverage) - cmd.Diff.String(): verify output formatting (was 0% coverage) - cmd.Exec/Image: error paths, return values, workdir, invalid language - cmd.Verify: image block skipping, multiple diffs, code without output - exec.Run: invalid interpreter, workdir with temp dir - exec.CopyImage: directory source, jpeg/svg/gif formats - exec.RunImage: empty output, script failure - markdown.Parse: empty input, commentary-only, malformed image refs, empty blocks - markdown.Write: empty blocks, image output, fenceFor edge cases Coverage improvements: - cmd: 78.2% -> 88.2% - exec: 83.7% -> 88.4% - markdown: 92.5% -> 94.6% - Notable: Pop 0%->90%, Diff.String 0%->100%, Run 91.7%->100%, parseImageRef 78.6%->100% https://claude.ai/code/session_01SJfKcEB7Sn2HromPSyhBgo
1 parent d531261 commit 4630ad6

7 files changed

Lines changed: 606 additions & 0 deletions

File tree

cmd/build_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,97 @@ func TestImageMarkdownRefEscapedBang(t *testing.T) {
348348
}
349349
}
350350

351+
func TestExecFileNotFound(t *testing.T) {
352+
_, _, err := Exec("/nonexistent/path/demo.md", "bash", "echo hello", "")
353+
if err == nil {
354+
t.Error("expected error for nonexistent file")
355+
}
356+
if !strings.Contains(err.Error(), "file not found") {
357+
t.Errorf("expected 'file not found' error, got: %v", err)
358+
}
359+
}
360+
361+
func TestExecReturnsOutput(t *testing.T) {
362+
dir := t.TempDir()
363+
file := filepath.Join(dir, "demo.md")
364+
365+
if err := Init(file, "Test", "dev"); err != nil {
366+
t.Fatal(err)
367+
}
368+
369+
output, exitCode, err := Exec(file, "bash", "echo hello world", "")
370+
if err != nil {
371+
t.Fatal(err)
372+
}
373+
if exitCode != 0 {
374+
t.Errorf("expected exit code 0, got %d", exitCode)
375+
}
376+
if !strings.Contains(output, "hello world") {
377+
t.Errorf("expected output to contain 'hello world', got %q", output)
378+
}
379+
}
380+
381+
func TestExecReturnsNonZeroExitCode(t *testing.T) {
382+
dir := t.TempDir()
383+
file := filepath.Join(dir, "demo.md")
384+
385+
if err := Init(file, "Test", "dev"); err != nil {
386+
t.Fatal(err)
387+
}
388+
389+
output, exitCode, err := Exec(file, "bash", "echo oops && exit 42", "")
390+
if err != nil {
391+
t.Fatal(err)
392+
}
393+
if exitCode != 42 {
394+
t.Errorf("expected exit code 42, got %d", exitCode)
395+
}
396+
if !strings.Contains(output, "oops") {
397+
t.Errorf("expected output 'oops', got %q", output)
398+
}
399+
}
400+
401+
func TestExecWithWorkdir(t *testing.T) {
402+
dir := t.TempDir()
403+
file := filepath.Join(dir, "demo.md")
404+
405+
if err := Init(file, "Test", "dev"); err != nil {
406+
t.Fatal(err)
407+
}
408+
409+
output, _, err := Exec(file, "bash", "pwd", "/tmp")
410+
if err != nil {
411+
t.Fatal(err)
412+
}
413+
if !strings.Contains(output, "/tmp") {
414+
t.Errorf("expected working directory /tmp in output, got %q", output)
415+
}
416+
}
417+
418+
func TestExecInvalidLanguage(t *testing.T) {
419+
dir := t.TempDir()
420+
file := filepath.Join(dir, "demo.md")
421+
422+
if err := Init(file, "Test", "dev"); err != nil {
423+
t.Fatal(err)
424+
}
425+
426+
_, _, err := Exec(file, "nonexistent_lang_xyz", "code", "")
427+
if err == nil {
428+
t.Error("expected error for invalid language")
429+
}
430+
}
431+
432+
func TestImageFileNotFound(t *testing.T) {
433+
err := Image("/nonexistent/path/demo.md", "img.png", "")
434+
if err == nil {
435+
t.Error("expected error for nonexistent document file")
436+
}
437+
if !strings.Contains(err.Error(), "file not found") {
438+
t.Errorf("expected 'file not found' error, got: %v", err)
439+
}
440+
}
441+
351442
func TestImageMarkdownRefBadPath(t *testing.T) {
352443
dir := t.TempDir()
353444
file := filepath.Join(dir, "demo.md")

cmd/pop_test.go

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
package cmd
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"strings"
7+
"testing"
8+
)
9+
10+
func TestPopRemovesCommentary(t *testing.T) {
11+
dir := t.TempDir()
12+
file := filepath.Join(dir, "demo.md")
13+
14+
if err := Init(file, "Test", "dev"); err != nil {
15+
t.Fatal(err)
16+
}
17+
if err := Note(file, "Hello world"); err != nil {
18+
t.Fatal(err)
19+
}
20+
21+
if err := Pop(file); err != nil {
22+
t.Fatal(err)
23+
}
24+
25+
content, err := os.ReadFile(file)
26+
if err != nil {
27+
t.Fatal(err)
28+
}
29+
if strings.Contains(string(content), "Hello world") {
30+
t.Error("expected commentary to be removed after pop")
31+
}
32+
if !strings.Contains(string(content), "# Test") {
33+
t.Error("expected title to remain after pop")
34+
}
35+
}
36+
37+
func TestPopRemovesCodeAndOutput(t *testing.T) {
38+
dir := t.TempDir()
39+
file := filepath.Join(dir, "demo.md")
40+
41+
if err := Init(file, "Test", "dev"); err != nil {
42+
t.Fatal(err)
43+
}
44+
if _, _, err := Exec(file, "bash", "echo hello", ""); err != nil {
45+
t.Fatal(err)
46+
}
47+
48+
if err := Pop(file); err != nil {
49+
t.Fatal(err)
50+
}
51+
52+
content, err := os.ReadFile(file)
53+
if err != nil {
54+
t.Fatal(err)
55+
}
56+
s := string(content)
57+
if strings.Contains(s, "echo hello") {
58+
t.Error("expected code block to be removed after pop")
59+
}
60+
if strings.Contains(s, "```output") {
61+
t.Error("expected output block to be removed after pop")
62+
}
63+
}
64+
65+
func TestPopFailsOnTitleOnly(t *testing.T) {
66+
dir := t.TempDir()
67+
file := filepath.Join(dir, "demo.md")
68+
69+
if err := Init(file, "Test", "dev"); err != nil {
70+
t.Fatal(err)
71+
}
72+
73+
err := Pop(file)
74+
if err == nil {
75+
t.Error("expected error when popping title-only document")
76+
}
77+
if !strings.Contains(err.Error(), "nothing to pop") {
78+
t.Errorf("expected 'nothing to pop' error, got: %v", err)
79+
}
80+
}
81+
82+
func TestPopFailsOnEmptyFile(t *testing.T) {
83+
dir := t.TempDir()
84+
file := filepath.Join(dir, "empty.md")
85+
if err := os.WriteFile(file, []byte(""), 0644); err != nil {
86+
t.Fatal(err)
87+
}
88+
89+
err := Pop(file)
90+
if err == nil {
91+
t.Error("expected error when popping empty document")
92+
}
93+
if !strings.Contains(err.Error(), "empty") {
94+
t.Errorf("expected 'empty' error, got: %v", err)
95+
}
96+
}
97+
98+
func TestPopNonexistentFile(t *testing.T) {
99+
err := Pop("/nonexistent/path/demo.md")
100+
if err == nil {
101+
t.Error("expected error for nonexistent file")
102+
}
103+
}
104+
105+
func TestPopMultipleTimes(t *testing.T) {
106+
dir := t.TempDir()
107+
file := filepath.Join(dir, "demo.md")
108+
109+
if err := Init(file, "Test", "dev"); err != nil {
110+
t.Fatal(err)
111+
}
112+
if err := Note(file, "A comment"); err != nil {
113+
t.Fatal(err)
114+
}
115+
if _, _, err := Exec(file, "bash", "echo hello", ""); err != nil {
116+
t.Fatal(err)
117+
}
118+
119+
// Pop exec (code + output)
120+
if err := Pop(file); err != nil {
121+
t.Fatal(err)
122+
}
123+
content, _ := os.ReadFile(file)
124+
s := string(content)
125+
if strings.Contains(s, "echo hello") {
126+
t.Error("expected code block removed")
127+
}
128+
if !strings.Contains(s, "A comment") {
129+
t.Error("expected commentary to remain")
130+
}
131+
132+
// Pop commentary
133+
if err := Pop(file); err != nil {
134+
t.Fatal(err)
135+
}
136+
content, _ = os.ReadFile(file)
137+
s = string(content)
138+
if strings.Contains(s, "A comment") {
139+
t.Error("expected commentary removed")
140+
}
141+
142+
// Pop on title-only should fail
143+
err := Pop(file)
144+
if err == nil {
145+
t.Error("expected error on title-only document")
146+
}
147+
}

cmd/verify_test.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"path/filepath"
66
"strings"
77
"testing"
8+
9+
"github.com/simonw/showboat/markdown"
810
)
911

1012
func TestVerifyPasses(t *testing.T) {
@@ -120,3 +122,119 @@ func TestVerifyWritesOutput(t *testing.T) {
120122
t.Errorf("output file should not contain tampered output, got: %s", updatedContent)
121123
}
122124
}
125+
126+
func TestDiffString(t *testing.T) {
127+
d := Diff{
128+
BlockIndex: 3,
129+
Expected: "hello\n",
130+
Actual: "world\n",
131+
}
132+
133+
s := d.String()
134+
if !strings.Contains(s, "block 3") {
135+
t.Errorf("expected 'block 3' in Diff.String(), got: %s", s)
136+
}
137+
if !strings.Contains(s, "expected: hello") {
138+
t.Errorf("expected 'expected: hello' in Diff.String(), got: %s", s)
139+
}
140+
if !strings.Contains(s, "actual: world") {
141+
t.Errorf("expected 'actual: world' in Diff.String(), got: %s", s)
142+
}
143+
}
144+
145+
func TestDiffStringTrimsTrailingNewlines(t *testing.T) {
146+
d := Diff{
147+
BlockIndex: 0,
148+
Expected: "foo\n\n\n",
149+
Actual: "bar\n\n",
150+
}
151+
152+
s := d.String()
153+
// Should not end with trailing newlines in expected/actual portions
154+
if strings.Contains(s, "expected: foo\n\n") {
155+
t.Errorf("expected trailing newlines to be trimmed, got: %q", s)
156+
}
157+
}
158+
159+
func TestVerifySkipsImageBlocks(t *testing.T) {
160+
dir := t.TempDir()
161+
file := filepath.Join(dir, "demo.md")
162+
163+
// Write a document with an image code block (should be skipped by verify)
164+
blocks := []markdown.Block{
165+
markdown.TitleBlock{Title: "Test", Timestamp: "2026-01-01T00:00:00Z", Version: "dev"},
166+
markdown.CodeBlock{Lang: "bash", Code: "echo hello", IsImage: true},
167+
markdown.ImageOutputBlock{AltText: "screenshot", Filename: "img.png"},
168+
}
169+
if err := writeBlocks(file, blocks); err != nil {
170+
t.Fatal(err)
171+
}
172+
173+
diffs, err := Verify(file, "", "")
174+
if err != nil {
175+
t.Fatal(err)
176+
}
177+
if len(diffs) != 0 {
178+
t.Errorf("expected no diffs for image-only document, got %d", len(diffs))
179+
}
180+
}
181+
182+
func TestVerifyNonexistentFile(t *testing.T) {
183+
_, err := Verify("/nonexistent/path/demo.md", "", "")
184+
if err == nil {
185+
t.Error("expected error for nonexistent file")
186+
}
187+
}
188+
189+
func TestVerifyMultipleBlocks(t *testing.T) {
190+
dir := t.TempDir()
191+
file := filepath.Join(dir, "demo.md")
192+
193+
if err := Init(file, "Test", "dev"); err != nil {
194+
t.Fatal(err)
195+
}
196+
if _, _, err := Exec(file, "bash", "echo one", ""); err != nil {
197+
t.Fatal(err)
198+
}
199+
if _, _, err := Exec(file, "bash", "echo two", ""); err != nil {
200+
t.Fatal(err)
201+
}
202+
203+
// Tamper with both outputs
204+
content, _ := os.ReadFile(file)
205+
s := string(content)
206+
s = strings.Replace(s, "```output\none\n```", "```output\nwrong1\n```", 1)
207+
s = strings.Replace(s, "```output\ntwo\n```", "```output\nwrong2\n```", 1)
208+
os.WriteFile(file, []byte(s), 0644)
209+
210+
diffs, err := Verify(file, "", "")
211+
if err != nil {
212+
t.Fatal(err)
213+
}
214+
if len(diffs) != 2 {
215+
t.Fatalf("expected 2 diffs, got %d", len(diffs))
216+
}
217+
}
218+
219+
func TestVerifyCodeBlockWithoutOutput(t *testing.T) {
220+
dir := t.TempDir()
221+
file := filepath.Join(dir, "demo.md")
222+
223+
// Write a document with a code block but no following output block
224+
blocks := []markdown.Block{
225+
markdown.TitleBlock{Title: "Test", Timestamp: "2026-01-01T00:00:00Z", Version: "dev"},
226+
markdown.CodeBlock{Lang: "bash", Code: "echo hello"},
227+
}
228+
if err := writeBlocks(file, blocks); err != nil {
229+
t.Fatal(err)
230+
}
231+
232+
// Should not error — code block without output is just skipped
233+
diffs, err := Verify(file, "", "")
234+
if err != nil {
235+
t.Fatal(err)
236+
}
237+
if len(diffs) != 0 {
238+
t.Errorf("expected 0 diffs for code block without output, got %d", len(diffs))
239+
}
240+
}

0 commit comments

Comments
 (0)