-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathconformance_test.go
More file actions
132 lines (106 loc) · 3.86 KB
/
Copy pathconformance_test.go
File metadata and controls
132 lines (106 loc) · 3.86 KB
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
package botapi
import (
"os"
"path/filepath"
"reflect"
"testing"
"github.com/PuerkitoBio/goquery"
"github.com/gotd/botapi/internal/botdoc"
)
// coveredByOtherMeans lists published methods the library satisfies through a
// different API shape than a same-named method, mapped to where.
var coveredByOtherMeans = map[string]string{
"getMe": "Bot.Self()",
}
// deferredMethods lists published methods that are feasible in principle but not
// yet implemented, each blocked on a missing gotd/td RPC or unclear MTProto
// mapping. They should move to an implementation as td gains support.
//
// Empty: every published method is implemented, covered by other means, or
// not-applicable below.
var deferredMethods = map[string]string{}
// notApplicableMethods lists published methods that do not apply to the
// MTProto-native model and are intentionally not implemented.
var notApplicableMethods = map[string]string{
"getUpdates": "MTProto-native: updates arrive on the persistent connection (Decision #2)",
"setWebhook": "no webhook surface (Decision #2)",
"deleteWebhook": "no webhook surface (Decision #2)",
"getWebhookInfo": "no webhook surface (Decision #2)",
"logOut": "HTTP Bot API server lifecycle method; not applicable over MTProto",
"close": "HTTP Bot API server lifecycle method; not applicable over MTProto",
}
// TestMethodConformance asserts that every method published in the Bot API docs
// is either implemented on *Bot, covered by other means, or explicitly
// acknowledged as deferred / not-applicable. It is a drift guard: when Telegram
// ships a new method, this test fails until the method is implemented or
// categorized. It also fails if an allowlist entry stops being a published
// method (so the lists cannot rot).
func TestMethodConformance(t *testing.T) {
api := loadAPI(t)
implemented := botMethodSet()
published := map[string]struct{}{}
var uncategorized []string
for _, def := range api.Methods {
name := def.Name
published[name] = struct{}{}
switch {
case implemented[goName(name)]:
case coveredByOtherMeans[name] != "":
case deferredMethods[name] != "":
case notApplicableMethods[name] != "":
default:
uncategorized = append(uncategorized, name)
}
}
if len(uncategorized) > 0 {
t.Errorf("API drift: %d published method(s) are neither implemented nor categorized: %v\n"+
"Implement them, or add to deferredMethods/notApplicableMethods in conformance_test.go.",
len(uncategorized), uncategorized)
}
// Guard against stale allowlists: every acknowledged name must still be a
// published method.
for _, lists := range []map[string]string{coveredByOtherMeans, deferredMethods, notApplicableMethods} {
for name := range lists {
if _, ok := published[name]; !ok {
t.Errorf("stale allowlist entry %q is no longer a published method; remove it", name)
}
}
}
}
// loadAPI extracts the Bot API surface from the committed documentation
// snapshot.
func loadAPI(t *testing.T) botdoc.API {
t.Helper()
path := filepath.Join("internal", "botdoc", "_testdata", "api.html")
f, err := os.Open(path)
if err != nil {
t.Fatalf("open api docs: %v", err)
}
defer func() { _ = f.Close() }()
doc, err := goquery.NewDocumentFromReader(f)
if err != nil {
t.Fatalf("parse api docs: %v", err)
}
return botdoc.Extract(doc)
}
// botMethodSet returns the set of exported method names on *Bot.
func botMethodSet() map[string]bool {
t := reflect.TypeFor[*Bot]()
set := make(map[string]bool, t.NumMethod())
for i := 0; i < t.NumMethod(); i++ {
set[t.Method(i).Name] = true
}
return set
}
// goName maps a Bot API method name (lowerCamelCase) to the Go method name that
// would implement it (UpperCamelCase): only the first letter changes.
func goName(apiName string) string {
if apiName == "" {
return ""
}
b := []byte(apiName)
if b[0] >= 'a' && b[0] <= 'z' {
b[0] -= 'a' - 'A'
}
return string(b)
}