snapshot of eee7b5cfc0776c68816d7fdaa8af9d7a0e0da2e6 Annotations about the code that implements koment.

packaging/naming_test.go

1 package packaging_test
2
3 import (
4 "fmt"
5 "os"
6 "path/filepath"
7 "regexp"
8 "sort"
9 "strings"
10 "testing"
11 )
12
13 const versionPlaceholder = "VERSION"
14
15 var (
16 buildMatrix = regexp.MustCompile(`for target in ([^;]+); do`)
17 archiveName = regexp.MustCompile(`name="koment_` + versionPlaceholder + `_\$\{os\}_\$\{arch\}"`)
18 archiveMentions = regexp.MustCompile(`koment_` + versionPlaceholder + `_([a-z0-9]+)_([a-z0-9]+)\.(tar\.gz|zip)`)
19 checksumMention = regexp.MustCompile(`koment_` + versionPlaceholder + `_checksums\.txt`)
20 )
21
22 var versionTokens = strings.NewReplacer(
23 "${VERSION}", versionPlaceholder,
24 "{{VERSION}}", versionPlaceholder,
25 "${version}", versionPlaceholder,
26 "$version", versionPlaceholder,
27 )
28
29 func repositoryFile(t *testing.T, path string) string {
30 t.Helper()
31 content, err := os.ReadFile(filepath.Join("..", filepath.FromSlash(path)))
32 if err != nil {
33 t.Fatalf("read %s: %v", path, err)
34 }
35 return versionTokens.Replace(string(content))
36 }
37
38 type archive struct {
39 os, arch, extension string
40 }
41
42 func (a archive) String() string {
43 return fmt.Sprintf("koment_%s_%s_%s.%s", versionPlaceholder, a.os, a.arch, a.extension)
44 }
45
46 func (a archive) binary() string {
47 if a.os == "windows" {
48 return "koment.exe"
49 }
50 return "koment"
51 }
52
53 const releaseWorkflow = ".github/workflows/release.yml"
54
55 func published(t *testing.T) map[archive]bool {
56 t.Helper()
57 workflow := repositoryFile(t, releaseWorkflow)
58
59 if !archiveName.MatchString(workflow) {
60 t.Fatalf("%s no longer builds koment_%s_<os>_<arch>; every consumer below is now guessing",
61 releaseWorkflow, versionPlaceholder)
62 }
63 matrix := buildMatrix.FindStringSubmatch(workflow)
64 if matrix == nil {
65 t.Fatalf("%s has no recognisable build matrix", releaseWorkflow)
66 }
67
68 archives := map[archive]bool{}
69 for _, target := range strings.Fields(matrix[1]) {
70 platform, architecture, found := strings.Cut(target, "/")
71 if !found {
72 t.Fatalf("build target %q is not os/arch", target)
73 }
74 archives[archive{os: platform, arch: architecture, extension: extensionFor(platform)}] = true
75 }
76 if len(archives) == 0 {
77 t.Fatal("the release workflow builds nothing")
78 }
79 return archives
80 }
81
82 func extensionFor(platform string) string {
83 if platform == "windows" {
84 return "zip"
85 }
86 return "tar.gz"
87 }
88
89 var consumers = map[string]string{
90 "the Homebrew tap": "packaging/homebrew/koment.rb.tmpl",
91 "the Scoop bucket": "packaging/scoop/koment.json.tmpl",
92 "the WinGet bundle": "packaging/winget/JanPuc.Koment.installer.yaml.tmpl",
93 }
94
95 const setupAction = "action.yml"
96
97 var (
98 actionArchive = regexp.MustCompile(`archive="koment_` + versionPlaceholder + `_\$\{platform\}_\$\{arch\}\.(tar\.gz|zip)"`)
99 actionPlatform = regexp.MustCompile(`platform=([a-z0-9]+)\s*;;`)
100 actionArchitecure = regexp.MustCompile(`arch=([a-z0-9]+)\s*;;`)
101 )
102
103 func requestedByTheSetupAction(t *testing.T) map[archive]bool {
104 t.Helper()
105 content := repositoryFile(t, setupAction)
106
107 template := actionArchive.FindStringSubmatch(content)
108 if template == nil {
109 t.Fatalf("%s no longer builds its download name from the runner's platform and architecture", setupAction)
110 }
111 platforms := actionPlatform.FindAllStringSubmatch(content, -1)
112 architectures := actionArchitecure.FindAllStringSubmatch(content, -1)
113 if platforms == nil || architectures == nil {
114 t.Fatalf("%s has no recognisable runner mapping", setupAction)
115 }
116
117 requested := map[archive]bool{}
118 for _, platform := range platforms {
119 for _, architecture := range architectures {
120 requested[archive{os: platform[1], arch: architecture[1], extension: template[1]}] = true
121 }
122 }
123 return requested
124 }
125
126 func TestEveryPackagedArchiveIsOneTheReleaseWorkflowBuilds(t *testing.T) {
127 archives := published(t)
128
129 for consumer, path := range consumers {
130 content := repositoryFile(t, path)
131 mentions := archiveMentions.FindAllStringSubmatch(content, -1)
132 if mentions == nil {
133 t.Errorf("%s (%s) references no koment archive at all", consumer, path)
134 continue
135 }
136 for _, mention := range mentions {
137 referenced := archive{os: mention[1], arch: mention[2], extension: mention[3]}
138 if !archives[referenced] {
139 t.Errorf("%s (%s) downloads %s, which the release workflow never builds; buildable: %s",
140 consumer, path, referenced, sortedNames(archives))
141 }
142 }
143 }
144
145 for requested := range requestedByTheSetupAction(t) {
146 if !archives[requested] {
147 t.Errorf("the setup action (%s) resolves runners to %s, which the release workflow never builds; buildable: %s",
148 setupAction, requested, sortedNames(archives))
149 }
150 }
151 }
152
153 // An archive nothing installs is either a wasted build or a platform whose
154 // install path was forgotten. Both are worth failing over.
155 func TestEveryPublishedArchiveHasSomethingThatInstallsIt(t *testing.T) {
156 archives := published(t)
157
158 installed := map[archive]string{}
159 for consumer, path := range consumers {
160 content := repositoryFile(t, path)
161 for _, mention := range archiveMentions.FindAllStringSubmatch(content, -1) {
162 installed[archive{os: mention[1], arch: mention[2], extension: mention[3]}] = consumer
163 }
164 }
165 for requested := range requestedByTheSetupAction(t) {
166 installed[requested] = "the setup action"
167 }
168
169 for built := range archives {
170 if installed[built] == "" {
171 t.Errorf("the release workflow builds %s but no packaging channel installs it", built)
172 }
173 }
174 }
175
176 // The setup action verifies its download against the manifest the release
177 // workflow writes, so the two must agree on that file's name too.
178 func TestTheSetupActionAsksForTheChecksumManifestByItsPublishedName(t *testing.T) {
179 if !checksumMention.MatchString(repositoryFile(t, releaseWorkflow)) {
180 t.Fatalf("%s no longer writes koment_%s_checksums.txt", releaseWorkflow, versionPlaceholder)
181 }
182 if !checksumMention.MatchString(repositoryFile(t, "action.yml")) {
183 t.Error("action.yml would download a checksum manifest the release never publishes")
184 }
185 }
186
187 // Package managers put the extracted binary on PATH by name, so the name inside
188 // the archive is as much a public interface as the archive itself.
189 func TestPackagedBinaryNamesMatchWhatTheArchivesContain(t *testing.T) {
190 for _, expectation := range []struct {
191 consumer, path, needle string
192 platform string
193 }{
194 {"the Homebrew tap", "packaging/homebrew/koment.rb.tmpl", `bin.install "koment"`, "darwin"},
195 {"the WinGet bundle", "packaging/winget/JanPuc.Koment.installer.yaml.tmpl", "RelativeFilePath: koment.exe", "windows"},
196 {"the Scoop bucket", "packaging/scoop/koment.json.tmpl", `"bin": "koment.exe"`, "windows"},
197 {"the setup action", "action.yml", `-C "$target" koment`, "linux"},
198 } {
199 want := archive{os: expectation.platform}.binary()
200 if !strings.Contains(expectation.needle, want) {
201 t.Fatalf("test is inconsistent: %q does not name %q", expectation.needle, want)
202 }
203 if !strings.Contains(repositoryFile(t, expectation.path), expectation.needle) {
204 t.Errorf("%s (%s) no longer installs %q; the release workflow packages that name for %s",
205 expectation.consumer, expectation.path, want, expectation.platform)
206 }
207 }
208 }
209
210 // Homebrew installs from the archive root and the setup action extracts a bare
211 // member name, so nesting the binary in a directory would break both without
212 // changing a single file name (ADR 0109).
213 func TestBothArchiveShapesCarryTheBinaryAndItsLicenceAtTheRoot(t *testing.T) {
214 workflow := repositoryFile(t, releaseWorkflow)
215
216 for shape, packaging := range map[string]string{
217 "the Windows zip": `zip -q "${name}.zip" "$binary" LICENSE README.md`,
218 "the POSIX tarball": `tar -czf "${name}.tar.gz" "$binary" LICENSE README.md`,
219 "the Windows binary": `[ "$os" = windows ] && binary=koment.exe`,
220 "the extracted entry": `tar -xzf "${work}/${archive}" -C "$target" koment`,
221 } {
222 source := releaseWorkflow
223 if shape == "the extracted entry" {
224 source = setupAction
225 }
226 if !strings.Contains(repositoryFile(t, source), packaging) {
227 t.Errorf("%s no longer packages the binary, LICENSE and README at the archive root: %s expected %q",
228 shape, source, packaging)
229 }
230 }
231
232 if strings.Contains(workflow, `tar -czf "${name}.tar.gz" -C`) {
233 t.Error("the tarball now nests its contents; Homebrew's bin.install and the setup action both read the archive root")
234 }
235 }
236
237 func sortedNames(archives map[archive]bool) string {
238 names := make([]string, 0, len(archives))
239 for a := range archives {
240 names = append(names, a.String())
241 }
242 sort.Strings(names)
243 return strings.Join(names, ", ")
244 }

Find an annotation

Search file paths, rationale, kinds, and authors.

moveEnter openEsc close