1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
package main

import (
	"reflect"
	"testing"
)

// sortManpages orders the manpages a directory renders below its listing. The
// input order must not matter, so each case is also run reversed.
func TestSortManpages(t *testing.T) {
	for _, tc := range []struct {
		name    string
		project string
		dir     string
		pages   []string
		want    []string
	}{
		{
			// The real case this was written for: timetrack/ holds its own
			// page plus a copy of the sfttime one, and used to show only the
			// first of them.
			name:    "the directory's own page comes first",
			project: "Profpatsch",
			dir:     "users/Profpatsch/timetrack",
			pages: []string{
				"users/Profpatsch/timetrack/sfttime.7",
				"users/Profpatsch/timetrack/timetrack.1",
			},
			want: []string{
				"users/Profpatsch/timetrack/timetrack.1",
				"users/Profpatsch/timetrack/sfttime.7",
			},
		},
		{
			// A name match outranks a lower section, so the directory's own
			// page leads even when another page sorts earlier by section.
			name:    "name match beats a lower section number",
			project: "proj",
			dir:     "dir",
			pages:   []string{"dir/other.1", "dir/dir.9"},
			want:    []string{"dir/dir.9", "dir/other.1"},
		},
		{
			name:    "at the root the project name is what matches",
			project: "netencode",
			dir:     "",
			pages:   []string{"aaa.1", "netencode.5"},
			want:    []string{"netencode.5", "aaa.1"},
		},
		{
			name:    "otherwise the lower section wins",
			project: "proj",
			dir:     "dir",
			pages:   []string{"dir/b.7", "dir/a.1"},
			want:    []string{"dir/a.1", "dir/b.7"},
		},
		{
			name:    "same section falls back to the path",
			project: "proj",
			dir:     "dir",
			pages:   []string{"dir/b.1", "dir/a.1"},
			want:    []string{"dir/a.1", "dir/b.1"},
		},
		{
			name:    "a single page is trivially ordered",
			project: "proj",
			dir:     "dir",
			pages:   []string{"dir/only.1"},
			want:    []string{"dir/only.1"},
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			for _, rev := range []bool{false, true} {
				pages := append([]string(nil), tc.pages...)
				if rev {
					for i, j := 0, len(pages)-1; i < j; i, j = i+1, j-1 {
						pages[i], pages[j] = pages[j], pages[i]
					}
				}
				sortManpages(pages, tc.project, tc.dir)
				if !reflect.DeepEqual(pages, tc.want) {
					t.Errorf("reversed=%v: got %v, want %v", rev, pages, tc.want)
				}
			}
		})
	}
}

// sortManpages must tolerate the empty and nil slices the collecting loops
// produce for a directory holding no manpage at all, which is the common case.
func TestSortManpagesEmpty(t *testing.T) {
	var nilPages []string
	sortManpages(nilPages, "proj", "dir")
	if nilPages != nil {
		t.Errorf("nil slice became %v", nilPages)
	}
	sortManpages([]string{}, "proj", "dir")
}

// The rendered set is capped (maxDirManpages) so one request cannot fan out
// into an unbounded number of mandoc subprocesses. writeProse does the
// truncation; this pins the slicing that stands in for it, including that a
// short list is left whole.
func TestManpageCap(t *testing.T) {
	truncate := func(pages []string) []string {
		if len(pages) > maxDirManpages {
			return pages[:maxDirManpages]
		}
		return pages
	}

	var many []string
	for i := 0; i < maxDirManpages+5; i++ {
		many = append(many, "dir/page.1")
	}
	if got := len(truncate(many)); got != maxDirManpages {
		t.Errorf("cap: got %d pages, want %d", got, maxDirManpages)
	}
	short := []string{"dir/a.1", "dir/b.1"}
	if got := truncate(short); !reflect.DeepEqual(got, short) {
		t.Errorf("under the cap: got %v, want %v", got, short)
	}
}

// The heading above each rendered manpage names it "name(section)" and links
// to its own source file.
func TestManpageTitleHTML(t *testing.T) {
	for _, tc := range []struct {
		name    string
		project string
		path    string
		want    string
	}{
		{
			name:    "section 1 page in a nested directory",
			project: "Profpatsch",
			path:    "users/Profpatsch/timetrack/timetrack.1",
			want: `<h1 class="manpage-title">` +
				`<a href="/Profpatsch/users/Profpatsch/timetrack/timetrack.1">timetrack(1)</a></h1>`,
		},
		{
			name:    "section 7 page",
			project: "Profpatsch",
			path:    "nix/buildGo/buildGo.7",
			want: `<h1 class="manpage-title">` +
				`<a href="/Profpatsch/nix/buildGo/buildGo.7">buildGo(7)</a></h1>`,
		},
		{
			name:    "page at the tree root",
			project: "netencode",
			path:    "netencode.5",
			want: `<h1 class="manpage-title">` +
				`<a href="/netencode/netencode.5">netencode(5)</a></h1>`,
		},
		{
			// A dotted name must keep everything before the SECTION
			// extension, not just the first segment.
			name:    "dotted page name",
			project: "proj",
			path:    "dir/foo.bar.1",
			want: `<h1 class="manpage-title">` +
				`<a href="/proj/dir/foo.bar.1">foo.bar(1)</a></h1>`,
		},
		{
			// Paths come from the ingested tree, so a name carrying HTML
			// metacharacters must be escaped in both the href and the text
			// rather than reaching the page as markup.
			name:    "name needing HTML escaping",
			project: "proj",
			path:    `dir/a&b<c>.1`,
			want: `<h1 class="manpage-title">` +
				`<a href="/proj/dir/a&amp;b&lt;c&gt;.1">a&amp;b&lt;c&gt;(1)</a></h1>`,
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			got := manpageTitleHTML(tc.project, tc.path)
			if got != tc.want+"\n" {
				t.Errorf("got  %q\nwant %q", got, tc.want+"\n")
			}
		})
	}
}