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