| 📦 Turbo Python 6fc62ea k33g 12h ago | 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "encoding/xml" |
| 5 | "html" |
| 6 | "os" |
| 7 | "os/exec" |
| 8 | "regexp" |
| 9 | "sort" |
| 10 | "strings" |
| 11 | "testing" |
| 12 | ) |
| 13 | |
| 14 | // The package diagram is drawn by hand and read by people, so nothing in the |
| 15 | // build notices when it stops describing the code. It started as Turbo Rust's |
| 16 | // and shipped naming internal/rustlang and "the Rust scanner" — an error no |
| 17 | // test could see, because a diagram is a file nothing imports. |
| 18 | // |
| 19 | // These tests hold it to `go list`: the boxes are the packages this module |
| 20 | // actually imports, and the arrows between the two boxes that are ours are the |
| 21 | // imports that really exist. |
| 22 | |
| 23 | // diagramFile is the drawio the documentation links to. |
| 24 | const diagramFile = "docs/diagrams/packages.drawio" |
| 25 | |
| 26 | // mxFile is as much of drawio's format as these tests need: every cell, with |
| 27 | // its label, and — for an arrow — the two cells it joins. |
| 28 | type mxFile struct { |
| 29 | Host string `xml:"host,attr"` |
| 30 | Cells []mxCell `xml:"diagram>mxGraphModel>root>mxCell"` |
| 31 | } |
| 32 | |
| 33 | type mxCell struct { |
| 34 | ID string `xml:"id,attr"` |
| 35 | Value string `xml:"value,attr"` |
| 36 | Edge string `xml:"edge,attr"` |
| 37 | Source string `xml:"source,attr"` |
| 38 | Target string `xml:"target,attr"` |
| 39 | } |
| 40 | |
| 41 | // boldLabel is the package name inside a box: drawio stores the label as |
| 42 | // escaped HTML, and the name is the part in bold. |
| 43 | var boldLabel = regexp.MustCompile(`(?s)<b>(.*?)</b>`) |
| 44 | |
| 45 | // readDiagram parses the diagram, failing the test rather than returning an |
| 46 | // error — a diagram that will not parse is not a case any caller can handle. |
| 47 | func readDiagram(t *testing.T) mxFile { |
| 48 | t.Helper() |
| 49 | |
| 50 | raw, err := os.ReadFile(diagramFile) |
| 51 | if err != nil { |
| 52 | t.Fatalf("reading %s: %v", diagramFile, err) |
| 53 | } |
| 54 | |
| 55 | var file mxFile |
| 56 | if err := xml.Unmarshal(raw, &file); err != nil { |
| 57 | t.Fatalf("parsing %s: %v", diagramFile, err) |
| 58 | } |
| 59 | return file |
| 60 | } |
| 61 | |
| 62 | // boxes maps each box's package name to the id the arrows use for it. |
| 63 | func boxes(t *testing.T, file mxFile) map[string]string { |
| 64 | t.Helper() |
| 65 | |
| 66 | found := map[string]string{} |
| 67 | for _, cell := range file.Cells { |
| 68 | if cell.Edge == "1" || cell.Value == "" { |
| 69 | continue |
| 70 | } |
| 71 | label := html.UnescapeString(cell.Value) |
| 72 | // A box's package name is the part in bold, where there is one; the |
| 73 | // third-party box carries its name plain, with nothing to tell apart |
| 74 | // from it. |
| 75 | if match := boldLabel.FindStringSubmatch(label); match != nil { |
| 76 | label = match[1] |
| 77 | } |
| 78 | found[label] = cell.ID |
| 79 | } |
| 80 | return found |
| 81 | } |
| 82 | |
| 83 | // imports asks the toolchain what a package imports, shortened to the names the |
| 84 | // diagram uses: the last element for a turbo-core package, the module-relative |
| 85 | // path for one of ours, and "tcell/v2" for the one third-party dependency. |
| 86 | func imports(t *testing.T, pkg string) []string { |
| 87 | t.Helper() |
| 88 | |
| 89 | out, err := exec.Command("go", "list", "-f", `{{join .Imports "\n"}}`, pkg).Output() |
| 90 | if err != nil { |
| 91 | t.Fatalf("go list %s: %v", pkg, err) |
| 92 | } |
| 93 | |
| 94 | var names []string |
| 95 | for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") { |
| 96 | switch { |
| 97 | case strings.HasPrefix(line, "rickub.com/turbo-editors/turbo-core/"): |
| 98 | names = append(names, strings.TrimPrefix(line, "rickub.com/turbo-editors/turbo-core/")) |
| 99 | case strings.HasPrefix(line, "rickub.com/turbo-editors/turbo-python/"): |
| 100 | names = append(names, strings.TrimPrefix(line, "rickub.com/turbo-editors/turbo-python/")) |
| 101 | case strings.HasPrefix(line, "github.com/gdamore/tcell/"): |
| 102 | names = append(names, "tcell/v2") |
| 103 | } |
| 104 | } |
| 105 | sort.Strings(names) |
| 106 | return names |
| 107 | } |
| 108 | |
| 109 | // The boxes are exactly the packages the two packages of this module import, |
| 110 | // plus the two packages themselves. A box for a package nothing imports is as |
| 111 | // wrong as a missing one: both tell a reader something untrue about the code. |
| 112 | func TestTheDiagramDrawsExactlyThePackagesThisModuleImports(t *testing.T) { |
| 113 | drawn := boxes(t, readDiagram(t)) |
| 114 | |
| 115 | want := map[string]bool{"main": true, "internal/pythonlang": true} |
| 116 | for _, pkg := range append(imports(t, "."), imports(t, "./internal/pythonlang")...) { |
| 117 | want[pkg] = true |
| 118 | } |
| 119 | |
| 120 | for name := range want { |
| 121 | if _, ok := drawn[name]; !ok { |
| 122 | t.Errorf("%s draws no box for %q", diagramFile, name) |
| 123 | } |
| 124 | } |
| 125 | for name := range drawn { |
| 126 | if !want[name] { |
| 127 | t.Errorf("%s draws a box for %q, which nothing in this module imports", diagramFile, name) |
| 128 | } |
| 129 | } |
| 130 | } |
| 131 | |
| 132 | // Every arrow leaving one of our two boxes is an import that exists. This is |
| 133 | // the half that caught the copied diagram: an arrow drawn out of a box labelled |
| 134 | // internal/rustlang cannot be checked at all until the box is named right. |
| 135 | func TestEveryArrowOutOfOurPackagesIsARealImport(t *testing.T) { |
| 136 | file := readDiagram(t) |
| 137 | drawn := boxes(t, file) |
| 138 | |
| 139 | byID := map[string]string{} |
| 140 | for name, id := range drawn { |
| 141 | byID[id] = name |
| 142 | } |
| 143 | |
| 144 | ours := map[string]string{"main": ".", "internal/pythonlang": "./internal/pythonlang"} |
| 145 | for _, cell := range file.Cells { |
| 146 | if cell.Edge != "1" { |
| 147 | continue |
| 148 | } |
| 149 | from, ok := byID[cell.Source] |
| 150 | if !ok { |
| 151 | t.Errorf("%s draws an arrow out of unknown cell %q", diagramFile, cell.Source) |
| 152 | continue |
| 153 | } |
| 154 | pkg, ok := ours[from] |
| 155 | if !ok { |
| 156 | continue |
| 157 | } |
| 158 | |
| 159 | to := byID[cell.Target] |
| 160 | if to == "internal/pythonlang" && from == "main" { |
| 161 | continue // main imports it under its full path, already shortened |
| 162 | } |
| 163 | if !slicesContain(imports(t, pkg), to) { |
| 164 | t.Errorf("%s draws %s → %s, but %s imports no such package", diagramFile, from, to, from) |
| 165 | } |
| 166 | } |
| 167 | } |
| 168 | |
| 169 | // The file's host attribute names the project it was drawn for. It is the one |
| 170 | // field a reader never sees and a copy always keeps. |
| 171 | func TestTheDiagramSaysWhichProjectItWasDrawnFor(t *testing.T) { |
| 172 | if host := readDiagram(t).Host; host != "turbo-python" { |
| 173 | t.Errorf("%s was drawn for %q, not turbo-python", diagramFile, host) |
| 174 | } |
| 175 | } |
| 176 | |
| 177 | // No label anywhere in the diagram names another editor in the family, or the |
| 178 | // language it edits. The copied diagram said "the Rust scanner" in prose that |
| 179 | // no identifier check would have looked at. |
| 180 | func TestNoLabelInTheDiagramNamesAnotherEditorsLanguage(t *testing.T) { |
| 181 | for _, cell := range readDiagram(t).Cells { |
| 182 | label := html.UnescapeString(cell.Value) |
| 183 | for _, other := range []string{"rustlang", "golang", "Rust", "Go ", "turbo-rust", "turbo-go"} { |
| 184 | if strings.Contains(label, other) { |
| 185 | t.Errorf("%s labels a cell %q, which names %q", diagramFile, label, other) |
| 186 | } |
| 187 | } |
| 188 | } |
| 189 | } |
| 190 | |
| 191 | // slicesContain says whether a sorted list holds a value. It is here rather |
| 192 | // than from the standard library's slices package so the test reads the same |
| 193 | // way in a checkout of any Go version this module supports. |
| 194 | func slicesContain(list []string, want string) bool { |
| 195 | for _, got := range list { |
| 196 | if got == want { |
| 197 | return true |
| 198 | } |
| 199 | } |
| 200 | return false |
| 201 | } |